# Goals (OKRs)

URL: https://developers.uidu.org/docs/domains/goals

> Read the workspace's objectives and key results, measure progress against time, and update a goal's values.

* **Use it for:** [Server integration](https://developers.uidu.org/docs/paths/server-integration.md) · [Custom app](https://developers.uidu.org/docs/paths/custom-app.md)
* **Reads:** Bearer (`apiKey`) — server-side only
* **Writes:** Bearer (`apiKey`) — server-side only
* **In a custom app:** with the signed-in member's session, no token to configure — see [Custom app](https://developers.uidu.org/docs/paths/custom-app.md)
* **CLI:** `uidu goals`, `uidu timeframes`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

Goals are native to every uidu workspace: an organisation's OKRs already live there. Read them
to build dashboards ("objectives behind schedule"), weekly check-in screens or reports — don't
copy them into a [Model](https://developers.uidu.org/docs/domains/models.md).

## Mental model

```
Timeframe   (name, startDate, endDate, year, active)     "Q4 2026"
  └─ Goal   objective — parentId: null
       ├─ metricKind, initialValue, currentValue, targetValue, progress, status, currency
       └─→ subgoals    key results — Goals whose parentId is the objective
```

* **Objectives and key results are the same type.** A key result is a Goal whose `parentId` is
  its objective. `listGoals` returns both, flat and newest first — filter `!g.parentId` for the
  objectives and group the rest by `parentId`. Without `first` it returns all of them.
  `getGoal` returns one goal with its key results as `subgoals` (a Relay connection:
  `subgoals.edges[].node`); `subgoalsCount` is on every goal.
* `metricKind` is `number`, `percentage`, `currency` (in `goal.currency`), `checkbox` or
  `subgoal` (measured by its key results).
* `status` is the owner's own judgement: `on_track`, `needs_attention`, `off_track`,
  `accomplished`. It is not computed from the numbers — show it next to `isGoalBehind`, it says
  something different.
* **Timeframes.** `listTimeframes` returns `id`, `name`, `startDate`, `endDate`, `year` and
  `active` (the current ones). `endDate` is the last day of the period, and the time helpers
  count it as a whole day.
* **Auth.** Reads work with any client that may see the workspace, **including a custom app's
  session token** — which sees the workspace-level goals and those of the Space the app sits in.
  The SDK asks only for the fields a session may read; a goal's owner, members and activity
  aren't part of what it reads (asking for them fails the whole query for a session).
  `updateGoal` needs an account Bearer token (`apiKey`), server-side: a custom app cannot write
  goals.

<Callout type="warn" title="Values are stored x100">
  `initialValue`, `currentValue` and `targetValue` never hold the number a person typed: uidu keeps
  them multiplied by 100 (a 45% target is `4500`, €1,200 is `120000`). `progress` is percent x100
  too — `10000` is done. Don't divide by hand: use the helpers below to read, and `toGoalRaw` to
  write.
</Callout>

The helpers are pure functions over a goal:

| Helper                                   | Returns                                                                                                                                                                                                                   |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `goalValue(goal, raw)`                   | A stored value in the unit a person typed: percent points for `percentage` (45 = 45%), whole units of `goal.currency` for `currency`, `1` or `0` for `checkbox`. `null` stays `null`.                                     |
| `toGoalRaw(value)`                       | The inverse, for writes: `toGoalRaw(45)` → `4500`, rounded to an integer.                                                                                                                                                 |
| `goalProgress(goal)`                     | How far the goal is, `0` to `1` — above `1` when it overshoots. Reads `goal.progress`.                                                                                                                                    |
| `goalTimeElapsed(goal, now?)`            | How much of the timeframe has passed, `0` to `1`, or `null` when the goal has no timeframe.                                                                                                                               |
| `isGoalBehind(goal, { tolerance, now })` | `true` when less is done than time has elapsed, by more than `tolerance` (default `0`; `0.1` = ten points), measured at `now` (default `new Date()`). `false` for a goal with no timeframe or one that is `accomplished`. |

## Use cases

### OKR dashboard of goals behind schedule

Goal: a server-rendered page listing this quarter's objectives, their key results, and which ones
are behind — next to what their owners say.

```ts
// app/okr/data.ts — server-only
import {
  listGoals,
  listTimeframes,
  goalValue,
  goalProgress,
  goalTimeElapsed,
  isGoalBehind,
} from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin'; // createClient({ workspace, apiKey })

export async function okrDashboard() {
  const [goals, timeframes] = await Promise.all([
    listGoals(uiduAdmin), // every goal, objectives and key results alike
    listTimeframes(uiduAdmin),
  ]);
  const current = new Set(timeframes.filter((t) => t.active).map((t) => t.id));

  const objectives = goals.filter((g) => !g.parentId && g.timeframe && current.has(g.timeframe.id));
  const keyResultsOf = (id: string) => goals.filter((g) => g.parentId === id);

  return objectives.map((g) => ({
    name: g.name,
    timeframe: g.timeframe?.name, // 'Q4 2026'
    done: goalProgress(g), // 0..1
    elapsed: goalTimeElapsed(g), // 0..1, null without a timeframe
    behind: isGoalBehind(g, { tolerance: 0.1 }),
    status: g.status, // the owner's view — show it alongside, don't replace it
    keyResults: keyResultsOf(g.id).map((kr) => ({
      name: kr.name,
      current: goalValue(kr, kr.currentValue), // 32 for 32%, 1200 for €1,200
      target: goalValue(kr, kr.targetValue),
      unit: kr.metricKind === 'currency' ? kr.currency : kr.metricKind,
    })),
  }));
}
```

* Render `done` as `${Math.round(done * 100)}%`; it can exceed 100% when a goal overshoots.
* `getGoal(uiduAdmin, { id })` is the cheaper call for a single objective's page: it returns the
  goal plus `subgoals.edges[].node`, each with the same fields.

### Show the Space's goals in a custom app

Goal: an app installed in a Space shows that Space's objectives behind schedule, with the
signed-in member's session and no token to configure.

```tsx
'use client';
import { useEffect, useState } from 'react';
import { useUiduApp } from '@uidu/react';
import { listGoals, isGoalBehind, goalProgress } from '@uidu/client';

type GoalRow = Awaited<ReturnType<typeof listGoals>>[number];

export function BehindSchedule() {
  const app = useUiduApp();
  const client = app.status === 'ready' ? app.client : null; // the member's session
  const [behind, setBehind] = useState<GoalRow[]>([]);

  useEffect(() => {
    if (!client) return;
    // workspace-level goals and those of the Space the app sits in
    listGoals(client).then((goals) =>
      setBehind(goals.filter((g) => !g.parentId && isGoalBehind(g, { tolerance: 0.1 }))),
    );
  }, [client]);

  return (
    <ul>
      {behind.map((g) => (
        <li key={g.id}>
          {g.name} — {Math.round(goalProgress(g) * 100)}% ({g.status})
        </li>
      ))}
    </ul>
  );
}
```

* Read-only: the session can't call `updateGoal`. For a check-in form, post to your own server,
  which writes with the Bearer (next use case). The bridge and its states:
  [Custom app](https://developers.uidu.org/docs/paths/custom-app.md).

### Weekly check-in from a server action

Goal: a form where an owner records this week's value and status; the server writes it.

```ts
'use server';
import { updateGoal, toGoalRaw } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

export async function checkIn(goalId: string, value: number, status: string) {
  const result = await updateGoal(uiduAdmin, {
    input: {
      id: goalId,
      attributes: {
        currentValue: toGoalRaw(value), // 32 (%) → 3200; never the typed number
        status, // 'on_track' | 'needs_attention' | 'off_track' | 'accomplished'
      },
    },
  });

  if (result?.errors?.length) {
    return { ok: false, errors: result.errors.map((e) => e.name) };
  }
  return { ok: true, progress: result?.goal?.progress }; // percent x100
}
```

* Values are raw integers: `toGoalRaw` for `currentValue`, `targetValue` and `initialValue` —
  for a `checkbox` goal, `toGoalRaw(1)` when done. The updated goal is at `result.goal` (no
  `.node`), with its recomputed `progress`.
* From a terminal: `uidu goals update <id> --attributes '{"currentValue":3200}'` — the CLI takes
  raw values too. See [CLI commands](https://developers.uidu.org/docs/tools/cli/commands.md).

## API at a glance

Every `@uidu/client` function in &#x2A;*Goals (OKRs)**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#goals) has the rest of the SDK.

| Function                                                                                       | Kind   | Auth   | CLI               | Description                                                                                                                                                                                                                                                                                              |
| ---------------------------------------------------------------------------------------------- | ------ | ------ | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`getGoal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts)         | read   | Bearer | `goals get`       | One goal with its key results (`subgoals`).                                                                                                                                                                                                                                                              |
| [`goalProgress`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts)    | helper | —      | —                 | How far the goal is, 0 to 1 (above 1 when it overshoots). `goal.progress` itself is percent x100: 10000 is done.                                                                                                                                                                                         |
| [`goalTimeElapsed`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts) | helper | —      | —                 | How much of the goal's timeframe has passed, 0 to 1, or `null` when it has none. A goal whose `goalProgress` is below this is behind schedule.                                                                                                                                                           |
| [`goalValue`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts)       | helper | —      | —                 | A stored goal value (`initialValue`, `currentValue`, `targetValue`) in the unit a person typed: uidu keeps every one of them x100. For `percentage` that is percent points (45 = 45%), for `currency` whole units of `goal.currency`, for `checkbox` 1 or 0. `null` stays `null`.                        |
| [`isGoalBehind`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts)    | helper | —      | —                 | Behind schedule: less done than time elapsed, by more than `tolerance` (0.1 = ten points). `false` for a goal with no timeframe or one already accomplished. The goal's own `status` (`off_track`, `needs_attention`) is its owner's judgement — show it next to this, don't replace one with the other. |
| [`listGoals`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts)       | read   | Bearer | `goals list`      | Every goal of the workspace (objectives and key results alike), newest first. Without `first`, all of them.                                                                                                                                                                                              |
| [`listTimeframes`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts)  | read   | Bearer | `timeframes list` | The workspace's timeframes (the periods goals run over, `active` marks the current ones) as a flat array; page with `first`/`after`.                                                                                                                                                                     |
| [`toGoalRaw`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts)       | helper | —      | —                 | The inverse of `goalValue`, for `updateGoal`.                                                                                                                                                                                                                                                            |
| [`updateGoal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/goals.ts)      | write  | Bearer | `goals update`    | Authoring (Bearer / apiKey). Returns the payload, incl. `errors`. `initialValue` / `currentValue` / `targetValue` are stored x100: pass `toGoalRaw(value)`, never the number a person typed.                                                                                                             |

**Types:** `Goal`, `Timeframe`

**CLI:**

| Entity       | list | get | create | update | delete |
| ------------ | :--: | :-: | :----: | :----: | :----: |
| `goals`      |   ✓  |  ✓  |    —   |    ✓   |    —   |
| `timeframes` |   ✓  |  —  |    —   |    —   |    —   |
