Goals (OKRs)
Read the workspace's objectives and key results, measure progress against time, and update a goal's values.
- Use it for: Server integration · Custom app
- 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
- CLI:
uidu goals,uidu timeframes - Auth: what each token can do and where it may run — Authentication
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.
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
parentIdis its objective.listGoalsreturns both, flat and newest first — filter!g.parentIdfor the objectives and group the rest byparentId. Withoutfirstit returns all of them.getGoalreturns one goal with its key results assubgoals(a Relay connection:subgoals.edges[].node);subgoalsCountis on every goal. metricKindisnumber,percentage,currency(ingoal.currency),checkboxorsubgoal(measured by its key results).statusis the owner's own judgement:on_track,needs_attention,off_track,accomplished. It is not computed from the numbers — show it next toisGoalBehind, it says something different.- Timeframes.
listTimeframesreturnsid,name,startDate,endDate,yearandactive(the current ones).endDateis 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).
updateGoalneeds an account Bearer token (apiKey), server-side: a custom app cannot write goals.
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.
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.
// 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
doneas${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 plussubgoals.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.
'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.
Weekly check-in from a server action
Goal: a form where an owner records this week's value and status; the server writes it.
'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:
toGoalRawforcurrentValue,targetValueandinitialValue— for acheckboxgoal,toGoalRaw(1)when done. The updated goal is atresult.goal(no.node), with its recomputedprogress. - From a terminal:
uidu goals update <id> --attributes '{"currentValue":3200}'— the CLI takes raw values too. See CLI commands.
API at a glance
Every @uidu/client function in Goals (OKRs), generated from the source — the full reference has the rest of the SDK.
| Function | Kind | Auth | CLI | Description |
|---|---|---|---|---|
getGoal | read | Bearer | goals get | One goal with its key results (subgoals). |
goalProgress | helper | — | — | How far the goal is, 0 to 1 (above 1 when it overshoots). goal.progress itself is percent x100: 10000 is done. |
goalTimeElapsed | 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 | 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 | 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 | read | Bearer | goals list | Every goal of the workspace (objectives and key results alike), newest first. Without first, all of them. |
listTimeframes | 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 | helper | — | — | The inverse of goalValue, for updateGoal. |
updateGoal | 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 | ✓ | — | — | — | — |