# Overview

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

> Build websites, server integrations and custom apps on the uidu platform.

The uidu SDK gives you typed access to everything a uidu workspace holds — its CMS, forms, events,
contacts, people, goals and more — from TypeScript, React and the terminal.

## What are you building?

<Card title="A public website" href="https://developers.uidu.org/docs/paths/public-website.md">
  A site, blog, careers page or event listing rendered from uidu, with forms that write back.
  Public token for reads, a server action for writes.
</Card>

<Card title="A server integration" href="https://developers.uidu.org/docs/paths/server-integration.md">
  An internal dashboard, a sync job, a provisioning script — reading and writing the workspace's
  private data from a server you control.
</Card>

<Card title="An app inside uidu" href="https://developers.uidu.org/docs/paths/custom-app.md">
  An app members open inside a uidu Space. It acts as the signed-in member, with no token to
  configure, and keeps its own data in Models.
</Card>

Each path is a step-by-step guide. The rest of the docs back it up:
[Foundations](https://developers.uidu.org/docs/foundations/authentication.md) explain what applies everywhere (authentication,
the data model, writes, errors), the **Domains** pages cover one area of uidu each, and
[Reference](https://developers.uidu.org/docs/reference/client.md) lists every export.

## With an AI assistant

Every page has a Markdown version — add `.md` to its URL, or use **Copy Markdown** / **Open in
Claude** at the top of the page. For an agent working on your code:

* [`/llms.txt`](/llms.txt) — the index of these docs, one line per page, linking the Markdown versions.
* [`/llms-full.txt`](/llms-full.txt) — every page in one file.
* [`/sdk-manifest.json`](/sdk-manifest.json) — every export of the SDK with its domain, auth,
  signature and CLI command, generated from the source.
* **`https://developers.uidu.org/mcp`** — the same docs as MCP tools (search, read a page, look up a
  function). See [Docs MCP server](https://developers.uidu.org/docs/tools/docs-mcp.md) to connect Claude, ChatGPT or Cursor.

## Packages

| Package            | What it is                                                                              | Reference                                           |
| ------------------ | --------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `@uidu/client`     | Typed GraphQL client. Framework-agnostic, server-first, no React dependency.            | 177 exports — [reference](https://developers.uidu.org/docs/reference/client.md)   |
| `@uidu/react`      | React provider, hooks and components on top of @uidu/client.                            | 14 exports — [reference](https://developers.uidu.org/docs/reference/react.md)     |
| `@uidu/app-bridge` | The browser half of a custom app running inside uidu: handshake + session token.        | 8 exports — [reference](https://developers.uidu.org/docs/reference/app-bridge.md) |
| `@uidu/cli`        | The `uidu` command: read and author a workspace from the terminal, built for AI agents. | 32 entities — [commands](https://developers.uidu.org/docs/tools/cli/commands.md)  |
| `create-uidu-app`  | Scaffolds a new uidu-powered app.                                                       | `npx create-uidu-app`                               |
| `@uidu/skills`     | Agent skills (Claude Code, Cursor…) for building on uidu.                               | `npx skills add uidu-org/api.js`                    |

## What the SDK covers

Every domain `@uidu/client` can read or write, generated from the source. Each links to its guide;
the [client reference](https://developers.uidu.org/docs/reference/client.md) lists every function with its auth and CLI command.
What the platform can do that the SDK doesn't cover yet is on the [Roadmap](https://developers.uidu.org/docs/reference/roadmap.md).

* [Client & utilities](https://developers.uidu.org/docs/foundations/data-model.md) — `createClient`, `fromBridge`, `normalizeFieldValueContent`, `normalizeFieldValuesAttributes`, `paginate`
* [CMS — Sites](https://developers.uidu.org/docs/domains/cms.md) · CLI: `sites` — `createSite`, `getPageBySlug`, `getSingletonBlock`, `getSite`, `getSiteByDomain`, `listSitePages`
* [CMS — Projects (legacy)](https://developers.uidu.org/docs/domains/cms.md) · CLI: `projects` — `createFieldValue`, `createPage`, `createPageBlock`, `createProject`, `getPage`, `getTemplate`, `listPages`, `listProjects`
* [Stories](https://developers.uidu.org/docs/domains/stories.md) · CLI: `stories` — `createStory`, `getStory`, `listStories`, `updateStory`
* [Help center](https://developers.uidu.org/docs/domains/help-center.md) · CLI: `channel` — `createChannel`, `deleteChannel`, `getChannel`, `listChannels`, `updateChannel`
* [Knowledge base](https://developers.uidu.org/docs/domains/kb.md) · CLI: `kb-collections`, `kb-articles` — `createKbArticle`, `createKbCollection`, `deleteKbArticle`, `deleteKbCollection`, `getKbArticle`, `getKbCollection`, `listKbArticles`, `listKbCollections`, `updateKbArticle`, `updateKbCollection`
* [Search](https://developers.uidu.org/docs/domains/search.md) — `search`
* [Forms](https://developers.uidu.org/docs/domains/forms.md) · CLI: `forms` — `createForm`, `createFormResponse`, `deleteForm`, `getForm`, `listForms`, `updateForm`, `updateFormResponse`
* [Events](https://developers.uidu.org/docs/domains/events.md) · CLI: `events`, `attendances` — `createAttendance`, `createEvent`, `getEvent`, `listEvents`, `updateEvent`
* [Donations](https://developers.uidu.org/docs/domains/donations.md) · CLI: `donations` — `createDonation`, `createDonationCampaign`, `deleteDonationCampaign`, `getDonationCampaign`, `listDonationCampaigns`, `updateDonationCampaign`
* [Courses](https://developers.uidu.org/docs/domains/courses.md) · CLI: `courses` — `createCourse`, `deleteCourse`, `getCourse`, `getEnrollment`, `getLecture`, `listCourses`, `listEnrollments`, `listLectures`, `updateCourse`
* [Calls](https://developers.uidu.org/docs/domains/calls.md) · CLI: `calls` — `getCall`, `listCalls`
* [Jobs & applications](https://developers.uidu.org/docs/domains/jobs.md) · CLI: `jobs`, `applications` — `completeApplication`, `createApplication`, `getJob`, `listApplications`, `listJobs`, `updateApplication`
* [Contacts & deals](https://developers.uidu.org/docs/domains/contacts.md) · reads need Bearer · CLI: `contacts`, `deals` — `createContact`, `createDeal`, `deleteContact`, `getContact`, `getDeal`, `listContacts`, `listDeals`, `updateDeal`
* [Campaigns](https://developers.uidu.org/docs/domains/campaigns.md) · reads need Bearer · CLI: `campaigns` — `getCampaign`, `listCampaigns`, `listEmailCampaigns`
* [People (HR)](https://developers.uidu.org/docs/domains/people.md) · reads need Bearer · CLI: `employees`, `employments`, `offices`, `roles`, `ccnls`, `time-clocks`, `compensations`, `benefits`, `benefit-enrollments` — `contractEvents`, `createBenefit`, `createBenefitEnrollment`, `createCcnl`, `createCompensation`, `createEmployee`, `createEmployment`, `createTimeClock`, `deleteCompensation`, `deleteEmployment`, `getEmployee`, `listCcnls`, `listCircles`, `listEmployees`, `listEmploymentHistory`, `listKinds`, `listOffices`, `listRoles`, `terminateEmployment`, `updateCompensation`, `updateEmployee`, `updateEmployment`, `weekStartFor`
* [Goals (OKRs)](https://developers.uidu.org/docs/domains/goals.md) · reads need Bearer · CLI: `goals`, `timeframes` — `getGoal`, `goalProgress`, `goalTimeElapsed`, `goalValue`, `isGoalBehind`, `listGoals`, `listTimeframes`, `toGoalRaw`, `updateGoal`
* [Spaces, tasks & notes](https://developers.uidu.org/docs/domains/workspace.md) · CLI: `tasks`, `notes`, `spaces` — `createNote`, `createSpace`, `createTask`, `deleteNote`, `deleteSpace`, `deleteTask`, `updateNote`, `updateSpace`, `updateTask`
* [Bookings & calendars](https://developers.uidu.org/docs/domains/bookings.md) · reads need Bearer · CLI: `bookings` — `getBooking`, `getCalendar`, `getCalendarEvent`, `listBookings`, `listCalendarEvents`, `listCalendars`
* [Models (custom data)](https://developers.uidu.org/docs/domains/models.md) · reads need Bearer — `createField`, `createFieldOption`, `createModel`, `createModelItem`, `deleteField`, `deleteFieldOption`, `deleteFieldValue`, `deleteModelItem`, `ensureModel`, `getModel`, `getModelItem`, `listModelItems`, `listModels`, `toFieldValuesAttributes`, `updateField`, `updateFieldOption`, `updateFieldValue`, `updateModel`, `updateModelItem`
* [Provisioning & actions](https://developers.uidu.org/docs/foundations/writes.md) — `createWorkspace`, `executeAction`, `generateWorkspaceApiCredentials`

---

# Bookings &amp; Calendars

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

> Public calendars, calendar events, and bookings (appointments).

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser; Bearer for `getBooking`, `listBookings`
* **Writes:** none — read-only today
* **CLI:** `uidu bookings`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

Calendars and their events are the workspace's public schedule — opening hours, classes,
appointments on offer — and bookings are the reservations people made. Render the first on a
public site with the public token; read the second from your server.

## Mental model

```
Calendar            name, description, color ("#22c55e"), timeZone ("Europe/Rome")
  └─→ CalendarEvent name, startTime / endTime, allDay, timeZone, url,
                    primaryAddress, body (rich text — on getCalendarEvent only)

Booking             kind, number, status, currency, itemsCount, itemsTotal (cents),
                    paidAt / refundedAt / expiredAt / willExpireAt, contact,
                    fieldValuesByShortname (on getBooking only)
```

* **A `CalendarEvent` is lighter than an [`Event`](https://developers.uidu.org/docs/domains/events.md)**: no campaign, no
  attendance, no CMS body to build — a dated entry on a calendar. Use Events when people RSVP.
* **Two auth levels.** Calendars and calendar events are public reads: the public token works,
  including in the browser. Bookings are private data and need a Bearer (`apiKey`) client on the
  server — with a public token `listBookings` returns an empty list and `getBooking` returns
  `null`, no error. Nothing here writes: the domain is read-only today.
* **Times** (`startTime`, `endTime`) are ISO strings or `null`; an `allDay` event has no
  meaningful time of day. Each event and calendar carries its `timeZone` — format with it rather
  than the server's zone.
* **Money is in cents.** A booking's `itemsTotal` is an integer in the minor unit of its
  `currency`: divide by 100 to display it. `status` and `kind` are labels (strings); `number` is
  the human-readable booking number.
* **Filters.** `listCalendarEvents` narrows by `calendarIds` and by named `scopes` (e.g.
  `upcoming`). List items carry the event's `calendar` (`id`, `name`, `color`) and a short
  `primaryAddress` (`formattedAddress`, `city`); the full event, with `body` and the address's
  `name` and `countryCode`, comes from `getCalendarEvent`. `listCalendars` and `listBookings`
  page with `first`/`after` or `last`/`before`.

## Use cases

### Public calendar grid

Goal: a statically rendered page with a legend of calendars and the upcoming events grouped by
month.

```tsx
// app/calendar/page.tsx — Server Component
import { listCalendars, listCalendarEvents } from '@uidu/client';
import { uidu } from '@/lib/uidu'; // public token is enough

export default async function CalendarPage() {
  const [calendars, events] = await Promise.all([
    listCalendars(uidu, { first: 50 }),
    listCalendarEvents(uidu, { first: 100, scopes: ['upcoming'] }),
  ]);

  const byMonth = events.reduce<Record<string, typeof events>>((acc, e) => {
    if (!e.startTime) return acc;
    const month = e.startTime.slice(0, 7); // YYYY-MM
    (acc[month] ??= []).push(e);
    return acc;
  }, {});

  return (
    <div>
      <ul>
        {calendars.map((c) => (
          <li key={c.id} style={{ color: c.color ?? undefined }}>
            {c.name}
          </li>
        ))}
      </ul>
      {Object.entries(byMonth).map(([month, list]) => (
        <section key={month}>
          <h2>{month}</h2>
          <ul>
            {list.map((e) => (
              <li key={e.id} style={{ borderColor: e.calendar.color ?? undefined }}>
                <a href={`/calendar/${e.id}`}>{e.name}</a>{' '}
                {new Date(e.startTime!).toLocaleString(undefined, {
                  timeZone: e.timeZone ?? undefined,
                  dateStyle: 'medium',
                  timeStyle: e.allDay ? undefined : 'short',
                })}
                {e.primaryAddress?.city && ` · ${e.primaryAddress.city}`}
              </li>
            ))}
          </ul>
        </section>
      ))}
    </div>
  );
}
```

* To show one calendar only, pass `calendarIds: [calendarId]`; `getCalendar(uidu, { id })`
  returns its name, color and time zone for the page header.
* One page of events is usually enough for "upcoming"; for a full archive walk every page with
  `paginate` and `ListCalendarEventsDocument` — see
  [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).

### Calendar event page

Goal: the detail page each grid item links to, with the event's rich-text description.

```tsx
// app/calendar/[id]/page.tsx
import { notFound } from 'next/navigation';
import { getCalendarEvent } from '@uidu/client';
import { RichText } from '@uidu/react';
import { uidu } from '@/lib/uidu';

export default async function CalendarEventPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const event = await getCalendarEvent(uidu, { id });
  if (!event) notFound();

  return (
    <article>
      <p>{event.calendar.name}</p>
      <h1>{event.name}</h1>
      {event.primaryAddress && <p>{event.primaryAddress.formattedAddress}</p>}
      {event.body && <RichText doc={event.body} />} {/* Tiptap JSON */}
      {event.url && <a href={event.url}>More information</a>}
    </article>
  );
}
```

### Bookings report on the server

Goal: an internal page or export listing recent bookings with their customer and amount.

```ts
// app/admin/bookings/data.ts — server-only
import { listBookings, getBooking } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin'; // createClient({ workspace, apiKey })

const money = (cents: number | null | undefined, currency: string | null | undefined) =>
  cents == null
    ? '—'
    : new Intl.NumberFormat(undefined, { style: 'currency', currency: currency ?? 'EUR' }).format(
        cents / 100, // itemsTotal is in cents
      );

export async function recentBookings() {
  const bookings = await listBookings(uiduAdmin, { first: 25 }); // [] with a public token

  return bookings.map((b) => ({
    number: b.number,
    kind: b.kind,
    status: b.status,
    paid: Boolean(b.paidAt),
    items: b.itemsCount,
    total: money(b.itemsTotal, b.currency),
    customer: b.contact?.name ?? b.contact?.email,
  }));
}

export async function bookingDetail(id: string) {
  const booking = await getBooking(uiduAdmin, { id }); // null with a public token
  return booking && {
    ...booking,
    answers: booking.fieldValuesByShortname, // custom fields, keyed by shortname
  };
}
```

* `getBooking` adds `paymentIntentId`, `refundedAt`, `updatedAt` and the custom-field map
  `fieldValuesByShortname` to what the list returns.
* From a terminal: `uidu bookings list --json`, `uidu bookings get <id> --json`.

## API at a glance

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

| Function                                                                                             | Kind | Auth         | CLI             | Description                                                                                                                            |
| ---------------------------------------------------------------------------------------------------- | ---- | ------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| [`getBooking`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)         | read | Bearer       | `bookings get`  | One booking with its contact, totals in cents and custom fields (`fieldValuesByShortname`), or null; a public token sees none.         |
| [`getCalendar`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)        | read | public token | —               | One calendar (name, color, time zone), or null.                                                                                        |
| [`getCalendarEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)   | read | public token | —               | One calendar event with its rich-text `body` and its calendar, or null.                                                                |
| [`listBookings`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)       | read | Bearer       | `bookings list` | The workspace's bookings as a flat array; page with `first`/`after` (or `last`/`before`); a public token gets an empty list.           |
| [`listCalendarEvents`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts) | read | public token | —               | Calendar events as a flat array, optionally narrowed by `calendarIds` and named `scopes` (e.g. `upcoming`); page with `first`/`after`. |
| [`listCalendars`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)      | read | public token | —               | The workspace's calendars as a flat array; page with `first`/`after` (or `last`/`before`).                                             |

**Types:** `Booking`, `Calendar`, `CalendarEvent`

**CLI:**

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

---

# Calls

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

> Fetch job openings, grant calls, and any other application-driven opportunity. Apply via the call's embedded form.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** none — read-only today
* **CLI:** `uidu calls`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

A **call** is anything in uidu that accepts applications: a job opening, a grant call, a residency,
a casting call. Each one carries its own application form, so a call page is a rich-text body plus
a form to submit. For job postings and their first-class applications, see
[Jobs & applications](https://developers.uidu.org/docs/domains/jobs.md).

## Mental model

```
Call                       ← listCalls (summaries) / getCall (full)
  ├─ kind                  'job' | 'open_call' | 'opportunity'
  ├─ body, cover           rich text (Tiptap JSON) and cover image URL
  ├─ quantity              number of openings
  ├─ salaryMin / salaryMax whole currency units
  ├─ expiresAt             the application deadline
  ├─ primaryAddress        for location-bound calls
  └─ form                  the application form — same shape as getForm()
        └─→ FormResponse   createFormResponse (Forms domain)
```

* **One primitive, three kinds.** `kind` is `job`, `open_call` or `opportunity`. `listCalls`
  returns all of them; filter on `kind` yourself, or use [`listJobs`](https://developers.uidu.org/docs/domains/jobs.md) for
  `job` only.
* **The list is a summary.** `listCalls` returns `id`, `name`, `cover`, `kind`, `status`,
  `quantity`, salaries, `publishedAt`, `expiresAt`, `publicUrl`, `publicPath`, `published` and a
  short `primaryAddress` (`formattedAddress`, `city`). There is no `body` and no `form`: fetch
  one call with `getCall` for those.
* **The form is inlined.** `getCall` selects `call.form` with the same `FormFields` fragment as
  `getForm`, so `call.form` goes straight into `<DynamicForm>` or any renderer you wrote for
  [Forms](https://developers.uidu.org/docs/domains/forms.md). Each question is at `call.form.inputs.edges[].node`.
* **Salaries are whole units, not cents** — unlike every other amount in uidu (see
  [Data model](https://developers.uidu.org/docs/foundations/data-model.md)). Both are `number | null`. The call's currency
  is not part of the SDK's selection, so pass it yourself when you format
  ([`formatSalaryRange`](https://developers.uidu.org/docs/domains/jobs.md#careers-page)).
* **No deadline filter.** `listCalls` takes only `first`/`after` and sends no filter, so filter
  on `expiresAt` yourself if the page should list open calls only.
* **The calls domain is read-only.** Applying is a write in another domain: submit the call's
  form with [`createFormResponse`](https://developers.uidu.org/docs/domains/forms.md#createformresponseclient-input-), or, for a
  job, record an application with [`createApplication`](https://developers.uidu.org/docs/domains/jobs.md#job-page-with-an-apply-form).
  Either way it runs in a server action with the `apiKey` client — see
  [Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## Use cases

The examples import `uidu` from `lib/uidu.ts`, a client built with `publicToken` and `apiKey`
and imported only from server code, as in [Authentication](https://developers.uidu.org/docs/foundations/authentication.md#bearer--the-workspace).

### Listing open calls

Goal: a page that lists every call still accepting applications, with its place and deadline.

```tsx
// app/calls/page.tsx
import { listCalls } from '@uidu/client';
import { uidu } from '@/lib/uidu';

export default async function CallsPage() {
  const calls = await listCalls(uidu, { first: 50 });
  const now = new Date();
  const open = calls
    .filter((c) => c.kind !== 'job') // jobs have their own careers page
    .filter((c) => !c.expiresAt || new Date(c.expiresAt as string) > now)
    .sort((a, b) =>
      String(a.expiresAt ?? '').localeCompare(String(b.expiresAt ?? '')),
    );

  return (
    <ul>
      {open.map((call) => (
        <li key={call.id}>
          <a href={`/calls/${encodeURIComponent(call.id)}`}>{call.name}</a>
          {call.primaryAddress?.city && ` · ${call.primaryAddress.city}`}
          {call.expiresAt &&
            ` · apply by ${new Date(call.expiresAt as string).toLocaleDateString()}`}
        </li>
      ))}
    </ul>
  );
}
```

* `listCalls` returns one page as a flat array (empty if none). To read every call, pass
  `ListCallsDocument` to `paginate` — see
  [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).
* Call ids are GlobalIDs: encode them in a URL and decode them in the route, as above.

### Open-call page with its application form

Goal: one call's page — body, deadline, and the application form, submitted from the server.

```tsx
// app/calls/[id]/page.tsx
import { getCall, createFormResponse } from '@uidu/client';
import { DynamicForm, RichText, type DynamicFormValues } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

export default async function CallPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const call = await getCall(uidu, { id: decodeURIComponent(id) });
  if (!call) notFound();

  const form = call.form;
  const formId = form?.id;

  async function apply(values: DynamicFormValues) {
    'use server';
    if (!formId)
      return { ok: false as const, errors: [{ name: 'No application form' }] };
    const result = await createFormResponse(uidu, {
      input: {
        attributes: {
          formId,
          fieldValuesAttributes: values.fieldValues.map((fv) => ({
            fieldId: fv.fieldId,
            content: { value: fv.value }, // always wrapped
          })),
          contactAttributes: values.contact && {
            email: values.contact.email,
            contactableAttributes: {
              kind: 'person',
              firstName: values.contact.firstName,
              lastName: values.contact.lastName || undefined,
            },
          },
        },
        event: 'complete!', // submit it, don't leave a draft
      },
    });
    return result?.errors?.length
      ? { ok: false as const, errors: result.errors }
      : { ok: true as const, meta: { responseId: result?.formResponse?.id } };
  }

  return (
    <article>
      <h1>{call.name}</h1>
      {call.expiresAt && (
        <p>
          Apply by {new Date(call.expiresAt as string).toLocaleDateString()}
        </p>
      )}
      {call.body && <RichText doc={call.body} />}
      {form && <DynamicForm form={form} action={apply} submitLabel="Apply" />}
    </article>
  );
}
```

* `getCall` returns `null` for an unknown id; `call.form` can be `null` too (a call that takes no
  applications).
* `<DynamicForm>` collects the candidate's first name, last name and email
  (`includeContactFields`, on by default) plus one value per visible question.
* `createFormResponse` doesn't throw on validation errors: check `errors`. A transport failure
  throws a `UiduError` — see [Errors](https://developers.uidu.org/docs/foundations/errors.md).
* **Attachments** (CV, portfolio): each media question carries its upload target at
  `call.form.inputs.edges[].node.field` — `uploadPresignedUrl` is a presigned POST as JSON
  (`{ url, fields }`), split into `uploadUrl` and `uploadParams`; follow the
  [file upload flow](https://developers.uidu.org/docs/domains/forms.md#file-uploads).
* For a call of kind `job`, record a first-class application instead, so it shows up on the
  call's board: [Job page with an apply form](https://developers.uidu.org/docs/domains/jobs.md#job-page-with-an-apply-form).

## API at a glance

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

| Function                                                                                 | Kind | Auth         | CLI          | Description                                                                                          |
| ---------------------------------------------------------------------------------------- | ---- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------- |
| [`getCall`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/calls.ts)   | read | public token | `calls get`  | One call (a job opening or open call) with its application `form`, same shape as `getForm`, or null. |
| [`listCalls`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/calls.ts) | read | public token | `calls list` | The workspace's calls as a flat array of summaries, without their forms; page with `first`/`after`.  |

**Types:** `Call`

**CLI:**

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

---

# Campaigns

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

> Marketing campaigns and email campaign analytics.

* **Use it for:** [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** Bearer (`apiKey`) — server-side only
* **Writes:** none — read-only today
* **CLI:** `uidu campaigns`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

The workspace's marketing campaigns and how their emails performed. Read-only today, and reads need
the account Bearer (`apiKey`): campaign data runs on your server, never in the browser.

## Mental model

```
Campaign                 name, status, scheduledAt, sentAt, target list
  └─→ campaignable       its channel: an EmailCampaign or a TextMessageCampaign
EmailCampaign            subject, replyTo, delivery metrics
  └─→ campaign           back to its Campaign (id, name, status)
```

* **A `Campaign` is the parent record**: `name` (plus an internal `innerName`), `status`, the
  `scheduledAt` / `sentAt` timestamps, and the `list` it targets. `getCampaign` adds
  `preferences`, a JSON blob with the campaign's settings (segment, channel options).
* **`status`** is `draft`, `scheduled` or `sent` — a campaign starts as a draft. `listCampaigns&#x60;
  filters by it, and by &#x2A;*`kind`**, the channel type: `'EmailCampaign'` or
  `'TextMessageCampaign'`.
* **An `EmailCampaign` is the email realization of a campaign**, and it's where the numbers
  are: `sentCount`, `deliveredCount`, `openedCount`, `clickedCount`, `uniqueOpens`,
  `uniqueClicks`, `lastOpenedAt`, `lastClickedAt`. `listEmailCampaigns` reads them for the whole
  workspace, each with its `campaign`; there is no per-campaign call and no CLI noun for it.
  Bounce, complaint and unsubscribe counts exist in uidu but the SDK doesn't fetch them yet.
* **`openRate` and `clickRate` are integer percentages of delivered emails**
  (opened ÷ delivered × 100, clicked ÷ delivered × 100), and `0` when nothing was delivered yet.
  Show them as `42%`, don't multiply again.
* Both lists return one page as a flat array, newest first; page with `first`/`after` or walk
  them with [`paginate`](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md) (`ListCampaignsDocument`,
  `ListEmailCampaignsDocument`). A client with a public token sees none — an empty list is first a
  sign of the wrong token ([Authentication](https://developers.uidu.org/docs/foundations/authentication.md)).

## Use cases

### Email performance dashboard

Goal: an internal page lists the recent email blasts with their delivery and engagement.

```tsx
// app/dashboard/emails/page.tsx — a Server Component
import { listEmailCampaigns } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

export default async function EmailsPage() {
  const emails = await listEmailCampaigns(uiduAdmin, { first: 25 });

  return (
    <table>
      <thead>
        <tr>
          <th>Campaign</th>
          <th>Subject</th>
          <th>Delivered</th>
          <th>Open rate</th>
          <th>Click rate</th>
        </tr>
      </thead>
      <tbody>
        {emails.map((e) => (
          <tr key={e.id}>
            <td>{e.campaign?.name}</td>
            <td>{e.subject}</td>
            <td>
              {e.deliveredCount} / {e.sentCount}
            </td>
            <td>{e.openRate}%</td>
            <td>{e.clickRate}%</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

* `uiduAdmin` is the server client built with `apiKey` — see
  [Server integration](https://developers.uidu.org/docs/paths/server-integration.md). Keep this a Server Component (or a route
  handler): the token must not reach the browser.
* The rates are already percentages of *delivered*: compare them with `deliveredCount`, not
  `sentCount`.

### What's going out next

Goal: a weekly report of the campaigns scheduled to send, and what each one targets.

```ts
import { getCampaign, listCampaigns } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

const scheduled = await listCampaigns(uiduAdmin, {
  first: 50,
  status: 'scheduled',
  kind: 'EmailCampaign', // leave out for every channel
});

const report = await Promise.all(
  scheduled.map(async (c) => {
    const full = await getCampaign(uiduAdmin, { id: c.id });
    return {
      name: c.name,
      at: c.scheduledAt,
      list: full?.list?.name, // the target list — only on getCampaign
    };
  }),
);
```

* A listed campaign has `id`, `name`, `innerName`, `status`, `scheduledAt`, `sentAt` and the
  timestamps; the target `list` and the `preferences` JSON come from `getCampaign`.
* From a terminal: `uidu campaigns list --json` and `uidu campaigns get <id> --json`.

## API at a glance

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

| Function                                                                                              | Kind | Auth   | CLI              | Description                                                                                                                                    |
| ----------------------------------------------------------------------------------------------------- | ---- | ------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| [`getCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/campaigns.ts)        | read | Bearer | `campaigns get`  | One campaign with its `preferences` and target `list`, or null; a public token sees none.                                                      |
| [`listCampaigns`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/campaigns.ts)      | read | Bearer | `campaigns list` | The workspace's campaigns as a flat array, optionally filtered by `kind` and `status`; page with `first`/`after`.                              |
| [`listEmailCampaigns`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/campaigns.ts) | read | Bearer | —                | The email blasts of the workspace's campaigns with their delivery metrics (sent, opened, clicked), as a flat array; page with `first`/`after`. |

**Types:** `Campaign`

**CLI:**

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

---

# CMS

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

> Read Sites (the new CMS) and legacy CMS Projects with typed GraphQL functions.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu projects`, `uidu sites`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

The CMS is where a workspace keeps its website: pages, the blocks placed on them, and the header
and footer they share. uidu has two CMS generations and `@uidu/client` reads both; new sites go on
**Sites**.

## Mental model

|                | **Sites** (new CMS)                                                                 | **Projects** (legacy CMS)             |
| -------------- | ----------------------------------------------------------------------------------- | ------------------------------------- |
| Container      | `Site`                                                                              | `Project`                             |
| Page type      | a page-kind Model                                                                   | `Template`                            |
| Page           | a ModelItem of a page type                                                          | `Page`                                |
| Block kind     | a block-kind Model                                                                  | `Block`                               |
| Block instance | a block ModelItem placed on a page                                                  | `PageBlock`                           |
| Functions      | `getSiteByDomain`, `getSite`, `getPageBySlug`, `listSitePages`, `getSingletonBlock` | `getPage`, `listPages`, `getTemplate` |

**New sites: use Sites.** Projects keep working unchanged for sites already deployed on them, and a
Project can be migrated to a Site from the CMS app. How Sites sit on top of Models:
[Data model](https://developers.uidu.org/docs/foundations/data-model.md).

**Sites.** A `Site` is `{ id, name, shortname, domain, preferences }` — `preferences` holds its theme
settings as JSON. You find it by the domain it is served on (`getSiteByDomain`) or by id
(`getSite`, for previews, where there is no domain to go by). Under it:

```
Site
├── page                  SitePage: { id, name, slug, publishedAt, status, model, fields, blocks }
│   └── blocks[]          SiteBlock: { id, name, model, fields, position, placementId }
└── singleton             header, footer, nav — same SitePage shape, fetched by shortname
```

* `fields` is the item's field values **keyed by field shortname** (`page.fields.title`), both on
  a page and on each block.
* `model` is the page type or block kind: `{ id, name, kind }`, `kind` being `page`, `block` or
  `singleton`.
* `blocks` come back &#x2A;*sorted by `position`**. `placementId` is the placement itself (the child
  FieldValue): what reordering or removing a block targets.
* `status` is `Draft`, `Published` or `Scheduled`. `getPageBySlug` returns published pages only,
  and `listSitePages` published pages only, unless you pass `includeDrafts: true`.
* `listSitePages` returns `SitePageSummary[]` (the page without `fields` and `blocks`), leaves
  singletons out, and keeps one page type with `pageType` — matched against the Model's id **or**
  its name. It reads every page of the Site and filters on your side of the call; pages come back
  in creation order, so sort them yourself.
* `getSingletonBlock` finds a singleton by its **Model's shortname** (`header`, `footer`) and
  returns the same `SitePage` shape, or null.

<Callout type="warn">
  Block kinds have a stable `shortname`, but the uidu API does not expose it on
  `Model` yet — key your component map on `block.model.name` until it does.
  `<PageBlocks>` from `@uidu/react` renders legacy Project pages only; for a
  Site you write the map yourself (below).
</Callout>

**Projects (legacy).** `Project → Template → Page → PageBlock`, each `PageBlock` with
`fieldValues[]`. `getPage` returns the page with:

* `metadata` — `metaTitle`, `metaDescription`, `metaKeywords`, `metaImage` (each `string | null`);
* `pageBlocks[]` — each with its own `shortname` and `templateBlock.block.shortname`, the block
  "type" that `<PageBlocks>` keys on;
* `fieldValues[]` — `field.shortname` is the key, `content` the value (JSON), and `linkedRecord`
  the record a field points at. When that record is a Form, its questions come along (as
  `linkedRecord.inputs`), ready for `<DynamicForm>`.

`listPages` returns `{ id, name, slug }` only — cheap, but **one Relay page** at a time;
`getTemplate` returns a template with every page that uses it and their blocks (without page
`metadata` or linked forms). `Page` and `Template` are inferred from the live schema, so they
update the next time codegen runs.

**Both generations.** Every function on this page is server-safe and has no React or framework
dependency — Node, Bun, Deno, the browser, edge runtimes. Every read works with a `publicToken`
client. A `get*` returns `null` when the page doesn't exist or isn't published; a failed request
**throws** a `UiduError` (`GRAPHQL_ERROR` for schema or permission errors, `NETWORK_ERROR` for
connectivity) — see [Errors](https://developers.uidu.org/docs/foundations/errors.md). Writes (`createSite`, and `createProject`,
`createPage`, `createPageBlock`, `createFieldValue` on Projects) need the Bearer client and return
their payload with `errors` instead of throwing: [Writes](https://developers.uidu.org/docs/foundations/writes.md).

## Use cases

### Every page of a Site, with its header

Goal: one route renders any page of the Site by slug, pre-rendered at build time, with the header
singleton on top.

```tsx
// src/lib/site.ts — one request per render, shared by layout and page
import { cache } from 'react';
import { getSiteByDomain } from '@uidu/client';
import { uidu } from './uidu';

export const getSite = cache(() => getSiteByDomain(uidu, { domain: 'www.acme.org' }));

// src/app/[slug]/page.tsx
import type { ComponentType } from 'react';
import { notFound } from 'next/navigation';
import {
  getPageBySlug,
  getSingletonBlock,
  listSitePages,
  type SiteBlock,
} from '@uidu/client';
import { RichText, toText } from '@uidu/react';
import { getSite } from '@/lib/site';
import { uidu } from '@/lib/uidu';

function Hero({ block }: { block: SiteBlock }) {
  return (
    <section>
      <h1>{toText(block.fields.title)}</h1>
      <RichText doc={block.fields.body} />
    </section>
  );
}

// Keyed by the block kind's name in uidu (block.model.name).
const components: Record<string, ComponentType<{ block: SiteBlock }>> = { Hero };

export async function generateStaticParams() {
  const site = await getSite();
  if (!site) return [];
  const pages = await listSitePages(uidu, { siteId: site.id });
  return pages.filter((p) => p.slug).map((p) => ({ slug: p.slug! }));
}

export default async function SitePageRoute({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const site = await getSite();
  if (!site) notFound();

  const [page, header] = await Promise.all([
    getPageBySlug(uidu, { siteId: site.id, slug }),
    getSingletonBlock(uidu, { siteId: site.id, shortname: 'header' }),
  ]);
  if (!page) notFound();

  return (
    <>
      {header && <header>{toText(header.fields.title)}</header>}
      <main>
        {page.blocks.map((block) => {
          const Component = components[block.model.name ?? ''];
          return Component ? <Component key={block.id} block={block} /> : null;
        })}
      </main>
    </>
  );
}
```

* A block whose kind isn't in the map renders nothing — add a fallback while you build the map out.
* `getSiteByDomain` returns `null` for a domain no Site is served on. `cache()` makes the layout and
  the page share one request.
* The same steps from an empty folder, with a contact form and events:
  [Public website](https://developers.uidu.org/docs/paths/public-website.md).

### A blog listing, a sitemap and draft previews

Goal: list the posts of one page type, emit a sitemap, and let editors preview unpublished pages.

```tsx
// src/app/blog/page.tsx
import Link from 'next/link';
import { listSitePages } from '@uidu/client';
import { uidu } from '@/lib/uidu';
import { getSite } from '@/lib/site';

export default async function BlogPage() {
  const site = await getSite();
  if (!site) return null;

  const posts = await listSitePages(uidu, { siteId: site.id, pageType: 'Blog post' });
  const newest = [...posts].sort((a, b) =>
    (b.publishedAt ?? '').localeCompare(a.publishedAt ?? ''),
  );

  return (
    <ul>
      {newest.map((p) => (
        <li key={p.id}>
          <Link href={`/${p.slug}`}>{p.name}</Link>
        </li>
      ))}
    </ul>
  );
}

// src/app/sitemap.ts
import type { MetadataRoute } from 'next';
import { listSitePages } from '@uidu/client';
import { uidu } from '@/lib/uidu';
import { getSite } from '@/lib/site';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const site = await getSite();
  if (!site?.domain) return [];
  const pages = await listSitePages(uidu, { siteId: site.id });
  return pages
    .filter((p) => p.slug)
    .map((p) => ({
      url: `https://${site.domain}/${p.slug}`,
      lastModified: p.publishedAt ?? undefined,
    }));
}

// In the page route: drafts only while Next's draft mode is on.
import { draftMode } from 'next/headers';

const { isEnabled } = await draftMode();
const page = await getPageBySlug(uidu, { siteId: site.id, slug, includeDrafts: isEnabled });
```

* `pageType: 'Blog post'` matches the page type's **name**; its Model id works too and survives a
  rename.
* `includeDrafts` is not gated by the token — anyone who can read the Site can pass it. Turn it on
  only behind your own preview check, as above.
* In a preview there may be no domain yet: resolve the Site with `getSite(uidu, { id })` instead.

### A site still on a legacy Project

Goal: render every page of a Project with `<PageBlocks>`, statically, including a contact form a
block links to.

```tsx
// src/app/[slug]/page.tsx
import { notFound } from 'next/navigation';
import { getPage, listPages, paginate, ListPagesDocument } from '@uidu/client';
import { PageBlocks } from '@uidu/react';
import { uidu } from '@/lib/uidu';
import { ContactForm, Feature, Hero } from '@/components/blocks';

const projectId = process.env.UIDU_PROJECT_ID!;

export async function generateStaticParams() {
  // listPages(uidu, { projectId }) is one page of results; walk them all.
  const pages = await paginate(uidu, ListPagesDocument, {
    variables: { projectId, first: 100 },
    extract: (data) => data.currentWorkspace?.project?.pages,
  });
  return pages
    .filter((p) => p.slug && p.slug !== 'home')
    .map((p) => ({ slug: p.slug! }));
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const page = await getPage(uidu, { projectId, slug });
  return {
    title: page?.metadata?.metaTitle ?? page?.name ?? undefined,
    description: page?.metadata?.metaDescription ?? undefined,
  };
}

export default async function CmsPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const page = await getPage(uidu, { projectId, slug });
  if (!page) notFound(); // doesn't exist, or unpublished

  return (
    <PageBlocks
      pageBlocks={page.pageBlocks}
      components={{ Header: Hero, feature1: Feature, ModuloContatto: ContactForm }}
    />
  );
}

// Inside ContactForm ({ block }): the form a field links to.
const formField = block.fieldValues?.find(
  (fv) => fv.linkedRecord && 'inputs' in fv.linkedRecord,
);
```

* `<PageBlocks>` keys each block on `templateBlock.block.shortname` (falling back to the block's own
  `shortname`); inside a block, `useFields(block.fieldValues)` gives `{ shortname: value }`. Both:
  [React components](https://developers.uidu.org/docs/tools/react/components.md).
* The project id comes from `uidu projects list` or `listProjects`. More on this route, ISR and a
  sitemap: [Dynamic CMS routes](https://developers.uidu.org/docs/recipes/dynamic-cms-routes.md).
* `getTemplate(uidu, { projectId, templateId })` returns the template with all its pages and their
  blocks in one request — handy to render a section of pages that share a layout.

## API at a glance

Every `@uidu/client` function in **CMS — Sites**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#sites) has the rest of the SDK.

| Function                                                                                         | Kind  | Auth         | CLI            | Description                                                                                                                                                                                             |
| ------------------------------------------------------------------------------------------------ | ----- | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createSite`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)        | write | Bearer       | `sites create` | Provisions a new Site in the workspace/account (`tenantId`). Bearer token (`apiKey`) — this is workspace admin territory, never a public write. Returns the mutation payload as-is, including `errors`. |
| [`getPageBySlug`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)     | read  | public token | —              | A page of the Site by slug, with its field values and its ordered blocks. Only published pages unless `includeDrafts`.                                                                                  |
| [`getSingletonBlock`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts) | read  | public token | —              | A singleton of the Site — the header, the footer, the nav — by its Model's shortname, with its field values and blocks.                                                                                 |
| [`getSite`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)           | read  | public token | —              | A Site by its id — for previews, where there is no domain to go by.                                                                                                                                     |
| [`getSiteByDomain`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)   | read  | public token | —              | The Site served on `domain` (e.g. `www.acme.org`), or null.                                                                                                                                             |
| [`listSitePages`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)     | read  | public token | —              | The Site's pages, for navigation and sitemaps: published only unless `includeDrafts`, singletons (header, footer) left out.                                                                             |

**Types:** `Site`, `SiteBlock`, `SiteFields`, `SiteModel`, `SitePage`, `SitePageSummary`

**CLI:**

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

## API at a glance

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

| Function                                                                                      | Kind  | Auth         | CLI              | Description                                                                                                                                                                   |
| --------------------------------------------------------------------------------------------- | ----- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createFieldValue`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts) | write | Bearer       | `field create`   | Create a field value, e.g. set a page block's field (`fieldId`, `content`). Returns the payload (with `errors`) or null; the new value is at `fieldValue.node`.               |
| [`createPage`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)       | write | Bearer       | `page create`    | Create a legacy CMS page from a `templateId`, with `slug` and optional `metadataAttributes` (there is no `projectId` attribute). Returns the payload (with `errors`) or null. |
| [`createPageBlock`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)  | write | Bearer       | `block create`   | Place a template block on a page (`pageId`, `templateBlockId`, both numeric). Returns the payload (with `errors`) or null.                                                    |
| [`createProject`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)    | write | Bearer       | `project create` | Create a legacy CMS project (`name`, `slug`, `domain`). Returns the payload (with `errors`) or null; the new project is at `project.node`.                                    |
| [`getPage`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)          | read  | public token | `page get`       | A legacy CMS page by project and `slug`, with its metadata and ordered `pageBlocks` (and any linked form's questions), or null.                                               |
| [`getTemplate`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)      | read  | public token | —                | A legacy CMS template by id, with every page it holds and their blocks, or null.                                                                                              |
| [`listPages`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)        | read  | public token | `pages list`     | The pages of a legacy CMS project (`id` and `slug`), one Relay page at a time; use `paginate` with `ListPagesDocument` for all of them.                                       |
| [`listProjects`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)     | read  | public token | `projects list`  | The workspace's CMS projects. A page lives in one, and `getPage` needs its id.                                                                                                |

**Types:** `Page`, `Template`

**CLI:**

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

---

# Contacts &amp; Deals

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

> CRM — the workspace's people and organizations, and the sales deals negotiated with them.

* **Use it for:** [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** Bearer (`apiKey`) — server-side only
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu contacts`, `uidu deals`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

The workspace's CRM: the people and organizations it is in touch with, and the sales deals it is
negotiating with them. Everything here, reads included, needs the account Bearer (`apiKey`), so it
runs on your server.

## Mental model

```
Contact                     email, phone, custom fields       "Maria Rossi" / "Acme srl"
  └─→ contactable           a person (User) or an Organization — holds the name
Pipeline
  └─→ Status                the stages of the pipeline        "Qualified", "Proposal", "Won"
        └─→ Deal            name, amount (cents), currency, deadline, winProbability
              ├─→ user          the person it is negotiated with
              └─→ organization  the organization it is negotiated with
```

* **A contact is a person or an organization.** The `Contact` carries the channels (email, phone,
  addresses, tags) and custom fields; the name lives on its **contactable** — a `User` for a
  person, an `Organization` for a company. `ContactAttributes` has &#x2A;*no `name`**: write
  `contactableAttributes: { firstName, lastName }` for a person, or `contactableType:
  'Organization'` with `contactableAttributes: { name }` for an organization. Without
  `contactableType`, uidu builds a person (a `User` of kind `lead`). Reading, `contact.name` is
  the contactable's name.
* **`listContacts` isn't paginated.** The field is a plain array, not a Relay connection: one
  call returns every contact the token may see, and there are no `first`/`after` arguments. On
  a very large workspace, that's a heavy call. A listed contact has `id`, `name`, `email`,
  `avatar`, `createdAt`; the address (`formattedPrimaryAddress`) and the custom-field map
  (`fieldValuesByShortname`) come only from `getContact`.
* **Deals live in a pipeline, at a status.** `createDeal` needs `pipelineId` and `statusId`;
  moving a deal along is `updateDeal` with another `statusId`. The SDK has no call that lists
  pipelines or their statuses: read the ids from the deals you already have (`deal.pipeline`,
  `deal.status`) or from uidu.
* **`amount` is in cents**, a `BigInt` that arrives as a string — `Number(deal.amount) / 100`
  before you show it, with the deal's `currency`. `winProbability` is a percentage, 0–100.
* **`deal.user` is a person record (`User`), not a Contact.** `userId` and `organizationId` on a
  deal take the id of the contact's person or organization, which the SDK's contact reads don't
  return today.
* `listDeals` returns one page as a flat array (`first`/`after`, optional search `params`); walk
  every page with [`paginate`](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md) and
  `ListDealsDocument`. `getDeal` adds the deal's `user` and its `publicUrl`.
* A client built with a public token doesn't fail here: it sees **no** contacts and no deals
  (`[]`, `null`). An empty CRM is first a sign of the wrong token —
  [Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## Use cases

### Lead from a website form into the CRM

Goal: someone signs up on your site, and they show up as a contact in uidu.

```ts
// app/actions/lead.ts
'use server';
import { createContact } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

export async function captureLead(lead: {
  email: string;
  firstName: string;
  lastName?: string;
  company?: string;
}) {
  const result = await createContact(uiduAdmin, {
    input: {
      attributes: lead.company
        ? {
            email: lead.email,
            contactableType: 'Organization',
            contactableAttributes: { name: lead.company },
          }
        : {
            email: lead.email,
            contactableAttributes: { firstName: lead.firstName, lastName: lead.lastName },
          },
    },
  });

  if (result?.errors?.length) return { ok: false, errors: result.errors.map((e) => e.name) };
  return { ok: true, id: result?.contact?.id }; // the payload carries only the new id
}
```

* A server action, never the browser: `createContact` needs the Bearer, like every write here.
* When the form is a uidu [Form](https://developers.uidu.org/docs/domains/forms.md), you don't call `createContact` yourself:
  pass `contactAttributes` (same shape — `email`, `contactableAttributes`) to
  `createFormResponse`, and the response and its contact are created together.
* `createContact` returns `{ errors, contact: { id } }`. A non-empty `errors` is a validation
  failure and nothing is written; see [Writes](https://developers.uidu.org/docs/foundations/writes.md).
* `deleteContact(uiduAdmin, { input: { id } })` removes one; the CLI does the same with
  `uidu contacts create --attributes '{…}'` and `uidu contacts delete <id>`.

### Pipeline dashboard on your server

Goal: an internal page shows each stage of the pipeline with its deals and their total value.

```ts
// app/api/pipeline/route.ts
import { paginate, ListDealsDocument } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

export async function GET() {
  const deals = await paginate(uiduAdmin, ListDealsDocument, {
    variables: { first: 100 },
    extract: (data) => data.currentWorkspace?.deals,
  });

  const stages = new Map<string, { status: string; totalCents: number; deals: string[] }>();
  for (const deal of deals) {
    const key = deal.status?.id ?? 'none';
    const stage = stages.get(key) ?? { status: deal.status?.name ?? 'No status', totalCents: 0, deals: [] };
    stage.totalCents += Number(deal.amount ?? 0); // BigInt, in cents
    stage.deals.push(`${deal.name} — ${deal.winProbability ?? 0}%`);
    stages.set(key, stage);
  }

  return Response.json(
    [...stages.values()].map((s) => ({ ...s, total: s.totalCents / 100 })),
  );
}
```

* `listDeals(uiduAdmin, { first: 50 })` is enough for a page of the most relevant deals;
  `paginate` is for totals over everything.
* Totals assume one currency. If the workspace sells in several, group by `deal.currency` too.
* A stage with no deals doesn't appear: the statuses come from the deals themselves.

### Open a deal and move it along

Goal: your server opens a deal when a lead qualifies, then moves it to the next status.

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

export async function openDeal(pipelineId: string, statusId: string) {
  const result = await createDeal(uiduAdmin, {
    input: {
      attributes: {
        name: 'Acme — annual plan',
        pipelineId,
        statusId,
        amount: 1_250_000, // €12,500.00, in cents
        currency: 'EUR',
        winProbability: 40,
        deadline: '2026-12-31T00:00:00Z',
      },
    },
  });
  if (result?.errors?.length) return { ok: false, errors: result.errors.map((e) => e.name) };
  return { ok: true, id: result?.deal?.node?.id }; // createDeal → deal.node
}

export async function moveDeal(id: string, statusId: string) {
  const result = await updateDeal(uiduAdmin, { input: { id, attributes: { statusId } } });
  if (result?.errors?.length) return { ok: false, errors: result.errors.map((e) => e.name) };
  return { ok: true, id: result?.deal?.id }; // updateDeal → deal
}
```

* The two payloads differ: `createDeal&#x60; returns the new deal at &#x2A;*`deal.node`**, `updateDeal&#x60; at
  &#x2A;*`deal`**. Both return only the id; `getDeal` for the rest.
* The full error handling (`errors` vs a thrown `UiduError`) is in
  [Server integration](https://developers.uidu.org/docs/paths/server-integration.md).
* From a terminal: `uidu deals create --attributes '{"pipelineId":"…","statusId":"…"}'` and
  `uidu deals update <id> --attributes '{"statusId":"…"}'`.

## API at a glance

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

| Function                                                                                         | Kind  | Auth   | CLI               | Description                                                                                                                                     |
| ------------------------------------------------------------------------------------------------ | ----- | ------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createContact`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `contacts create` | Create a CRM contact from `{ input: { attributes } }`. Returns the payload (with `errors`) or null.                                             |
| [`createDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer | `deals create`    | Create a deal in a pipeline (`attributes.pipelineId`, `statusId`). Returns the payload (with `errors`) or null. The new deal is at `deal.node`. |
| [`deleteContact`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `contacts delete` | Delete a contact by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                           |
| [`getContact`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)     | read  | Bearer | `contacts get`    | One contact with its address and custom fields (`fieldValuesByShortname`), or null; a public token sees none.                                   |
| [`getDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)        | read  | Bearer | `deals get`       | One deal with its pipeline, status and owner (`user`), or null; `amount` is in cents.                                                           |
| [`listContacts`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)   | read  | Bearer | `contacts list`   | Every contact the client may see, as an array: the field is not a connection, so there is no pagination.                                        |
| [`listDeals`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)      | read  | Bearer | `deals list`      | The workspace's deals as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                     |
| [`updateDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer | `deals update`    | Update a deal, e.g. move it to another `statusId`: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                |

**Types:** `Contact`, `Deal`

**CLI:**

| Entity     | list | get | create | update | delete |
| ---------- | :--: | :-: | :----: | :----: | :----: |
| `contacts` |   ✓  |  ✓  |    ✓   |    —   |    ✓   |
| `deals`    |   ✓  |  ✓  |    ✓   |    ✓   |    —   |

---

# Courses

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

> Courses, lectures, and enrollments — a lightweight LMS surface.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser; Bearer for `getEnrollment`, `listEnrollments`
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu courses`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

A lightweight LMS: courses made of ordered lectures, and the people enrolled in them. Courses and
lectures are public content you can render on a website; enrollments are per-person data you read
from your server.

## Mental model

```
Course                     ← listCourses / getCourse (public token)
  ├─ name, abstract, cover, kind, mode, tags, publicPath
  ├─ body                  rich text — getCourse only
  └─→ Lecture              ← listLectures / getLecture (public token)
        ├─ position        order within the course
        ├─ duration        minutes
        ├─ link            external URL, e.g. the video host
        ├─ body            rich text — getLecture only
        └─ previousLecture / nextLecture   { id, name, position } | null
Enrollment                 ← listEnrollments / getEnrollment (Bearer)
  ├─ course, contact       { id, name } / { id, name, email }
  ├─ currentLecture        where the person is
  ├─ aasmState
  └─ fieldValuesByShortname   custom fields — getEnrollment only
```

* **`kind` and `mode` are strings** from the course's settings: `kind` is `e_learning`, `ilt` or
  `vilt`; `mode` is `external` or `internal`. `tags` is free-form.
* **Lists are summaries.** `listCourses` has no `body`; `listLectures` has no `body` and no
  previous/next. Fetch one record with `getCourse` / `getLecture` for those.
* **Lectures are listed workspace-wide.** `listLectures` has no course argument: it returns
  lectures across every course, and you keep the ones whose `course.id` matches. It also
  returns one page, so a course whose lectures fall past that page comes back short — walk
  every page with `paginate` when you need a full course (see the first use case).
* **`orderBy`, `scopes`, `from`/`to`** are passed through to `listLectures` as strings;
  `orderBy: 'position'` gives course order. `listCourses` and `listEnrollments` take search
  `params` (`SearchParams`: `filters`, `sorting`, `options`) instead.
* **Enrollments are private.** They tie a contact to a course, so reading them needs the
  account Bearer (`apiKey`) on your server. With a public token the list comes back empty
  rather than failing. `aasmState` is the enrollment's state as a string; read it, don't
  hard-code a fixed set of values.
* **Writes** (`createCourse`, `updateCourse`, `deleteCourse`) take `{ input: { attributes } }`
  (plus `id` to update or delete), need the Bearer, and return the payload with `errors` — see
  [Writes](https://developers.uidu.org/docs/foundations/writes.md). Lectures and enrollments have no writes in the SDK yet.

## Use cases

The examples import `uidu` from `lib/uidu.ts`, a client built with `publicToken` and `apiKey`
and imported only from server code, as in [Authentication](https://developers.uidu.org/docs/foundations/authentication.md#bearer--the-workspace).

### Course page with its lectures

Goal: a course's page with its description and the ordered list of its lectures.

```tsx
// app/courses/[id]/page.tsx
import { getCourse, paginate, ListLecturesDocument } from '@uidu/client';
import { RichText } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

export default async function CoursePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const [course, lectures] = await Promise.all([
    getCourse(uidu, { id: decodeURIComponent(id) }),
    // Every lecture in the workspace, every page — then keep this course's.
    paginate(uidu, ListLecturesDocument, {
      variables: { first: 100, orderBy: 'position' },
      extract: (data) => data.currentWorkspace?.lectures,
    }),
  ]);
  if (!course) notFound();

  const courseLectures = lectures
    .filter((l) => l.course?.id === course.id)
    .sort((a, b) => (a.position ?? 0) - (b.position ?? 0));

  return (
    <article>
      <h1>{course.name ?? 'Untitled course'}</h1>
      <p>{course.abstract}</p>
      {course.body && <RichText doc={course.body} />}
      <ol>
        {courseLectures.map((lec) => (
          <li key={lec.id}>
            <a href={`/lectures/${encodeURIComponent(lec.id)}`}>{lec.name}</a>
            {lec.duration && ` (${lec.duration} min)`}
          </li>
        ))}
      </ol>
    </article>
  );
}
```

* For a catalogue page, `listCourses(uidu, { first: 20 })` returns the course summaries:
  `name`, `abstract`, `cover`, `kind`, `mode`, `tags`, `publicPath` (e.g. `/courses/intro-to-x`)
  and `publicUrl`.
* `listLectures(uidu, { first: 100, orderBy: 'position' })` is enough when the workspace has few
  lectures; `paginate` is what makes the list complete. More in
  [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).
* The sort after filtering keeps the order right even when lectures of different courses
  interleave across pages.

### Lecture page with previous / next

Goal: one lecture's content, with links to the lectures before and after it.

```tsx
// app/lectures/[id]/page.tsx
import { getLecture } from '@uidu/client';
import { RichText } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

const href = (id: string) => `/lectures/${encodeURIComponent(id)}`;

export default async function LecturePage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const lecture = await getLecture(uidu, { id: decodeURIComponent(id) });
  if (!lecture) notFound();

  const { course, previousLecture: prev, nextLecture: next } = lecture;

  return (
    <article>
      {course && (
        <a href={`/courses/${encodeURIComponent(course.id)}`}>{course.name}</a>
      )}
      <h1>{lecture.name}</h1>
      {lecture.duration && <p>{lecture.duration} min</p>}
      {lecture.link && <a href={lecture.link}>Watch</a>}
      {lecture.body && <RichText doc={lecture.body} />}
      <nav>
        {prev && <a href={href(prev.id)}>← {prev.name}</a>}
        {next && <a href={href(next.id)}>{next.name} →</a>}
      </nav>
    </article>
  );
}
```

* `previousLecture` / `nextLecture` are `{ id, name, position } | null`: no need to fetch the
  whole course to navigate.
* `getLecture` also returns `kind`, `cover`, `startDate` / `startTime` (for scheduled
  lectures), `published` and `draft`.

### Enrollment report

Goal: from your server, list who is enrolled in which course and where they are, and read one
enrollment's custom fields.

```ts
// app/api/enrollments/route.ts — server only, needs the apiKey client
import { listEnrollments, getEnrollment } from '@uidu/client';
import { uidu } from '@/lib/uidu';

export async function GET() {
  const enrollments = await listEnrollments(uidu, { first: 50 });

  const rows = enrollments.map((e) => ({
    course: e.course.name,
    person: e.contact.name,
    email: e.contact.email,
    state: e.aasmState,
    at: e.currentLecture?.name ?? null,
  }));

  // One enrollment in full: its custom fields as a { shortname: value } map.
  const first =
    enrollments[0] && (await getEnrollment(uidu, { id: enrollments[0].id }));

  return Response.json({ rows, fields: first?.fieldValuesByShortname ?? null });
}
```

* Empty with no error means the client has no `apiKey`: enrollments are invisible to a public
  token.
* `fieldValuesByShortname` (custom fields) and `currentLecture.position` are on `getEnrollment`
  only; the list has `currentLecture { id, name }`.
* To read every enrollment, pass `ListEnrollmentsDocument` to `paginate`.

## API at a glance

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

| Function                                                                                         | Kind  | Auth         | CLI              | Description                                                                                                                                      |
| ------------------------------------------------------------------------------------------------ | ----- | ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`createCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses create` | Create a course from `{ input: { attributes } }`. Returns the payload (with `errors`) or null.                                                   |
| [`deleteCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses delete` | Delete a course by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                             |
| [`getCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)       | read  | public token | `courses get`    | One course with its rich-text `body`, or null.                                                                                                   |
| [`getEnrollment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)   | read  | Bearer       | —                | One enrollment with its course, contact and custom fields (`fieldValuesByShortname`), or null.                                                   |
| [`getLecture`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)      | read  | public token | —                | One lecture with its `body`, its course and its `nextLecture` / `previousLecture` for navigation, or null.                                       |
| [`listCourses`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)     | read  | public token | `courses list`   | The workspace's courses as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                    |
| [`listEnrollments`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts) | read  | Bearer       | —                | Course enrollments as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                         |
| [`listLectures`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)    | read  | public token | —                | Lectures across every course as a flat array (filter by `course.id` yourself), with `scopes`, `orderBy`, `from`/`to`; page with `first`/`after`. |
| [`updateCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses update` | Update a course: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                   |

**Types:** `Course`, `Enrollment`, `Lecture`

**CLI:**

| Entity    | list | get | create | update | delete |
| --------- | :--: | :-: | :----: | :----: | :----: |
| `courses` |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |

---

# Donations

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

> Fetch donation campaigns and accept donations against uidu's fundraising API.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu donations`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

Donation campaigns are your workspace's fundraisers — a charity drive, a sponsorship pool, a
crowdfunding goal. You show them and their progress with the public token, and record each gift
with `createDonation` from your server.

## Mental model

* **`DonationCampaign`** — the fundraiser: `name`, a rich-text `body` (Tiptap JSON — render it with
  [`<RichText>`](https://developers.uidu.org/docs/tools/react/components.md)), `cover`, `kind`, `startsAt`, `publishedAt`,
  `publicPath` / `publicUrl`, a `primaryAddress`, and live totals: `goal`, `donationsAmount`,
  `donationsCount`, `donationsAverage&#x60;. It may carry a &#x2A;*`form`** — a donor questionnaire with the same
  shape as [Forms](https://developers.uidu.org/docs/domains/forms.md) — for anything beyond name and email.
* **`Donation`** — one gift to one campaign (`donationCampaignId`): `amount`, `currency`, the donor
  as a contact, and optionally a message (`body`), a `dedication` ("in memory of…") and the answers to
  the campaign's form (`formResponseAttributes`). It comes back with `id`, `amount`, `currency`,
  `createdAt` and `contact.id`.

Shapes and gotchas:

* **Money is in cents.** `goal`, `donationsAmount`, `donationsAverage` and a donation's `amount` are
  integers in the currency's minor unit: `5000` is 50.00. Divide by 100 to display, multiply by 100
  (and round) before you write. `currency` is an ISO code: `'EUR'`, `'USD'`…
* **Recording is not charging.** `createDonation` records the gift and the donor; taking the money is a
  separate step with your payment processor (Stripe, …), unless payment runs through uidu's checkout
  (`orderAttributes` / `subscriptionAttributes`, below).
* **A gift is not a campaign.** `createDonation` records a gift *to* a campaign;
  `createDonationCampaign` creates the campaign itself.
* Both mutations return the payload as-is — `{ errors, donation }` / `{ errors, donationCampaign }`,
  or null — with validation failures in `errors`, not thrown. Both need the `apiKey` client: a
  donation is submitted from a server action, never with the public token in the browser. See
  [Writes](https://developers.uidu.org/docs/foundations/writes.md) and [Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## Use cases

### Campaigns page with progress bars

Goal: list the active campaigns, each with how much it has raised against its goal.

```tsx
// app/donate/page.tsx — a Server Component
import Link from 'next/link';
import { listDonationCampaigns } from '@uidu/client';
import { uidu } from '@/lib/uidu';

const euros = (cents: number | null | undefined) =>
  ((cents ?? 0) / 100).toLocaleString('en', {
    style: 'currency',
    currency: 'EUR',
  });

export default async function CampaignsPage() {
  const campaigns = await listDonationCampaigns(uidu, { first: 20 });

  return (
    <ul>
      {campaigns.map((c) => {
        const pct = c.goal
          ? Math.min(100, ((c.donationsAmount ?? 0) / c.goal) * 100)
          : 0;
        return (
          <li key={c.id}>
            <Link href={`/donate/${encodeURIComponent(c.id)}`}>{c.name}</Link>
            <div className="h-2 rounded-full bg-slate-200">
              <div
                className="h-full rounded-full bg-emerald-500"
                style={{ width: `${pct}%` }}
              />
            </div>
            <p>
              {euros(c.donationsAmount)} raised of {euros(c.goal)} ·{' '}
              {c.donationsCount ?? 0} donors
            </p>
          </li>
        );
      })}
    </ul>
  );
}
```

* Summaries are cheap: `id`, `name`, `cover`, `goal`, `donationsAmount`, `donationsCount`,
  `startsAt`, `publishedAt`, `publicPath`. The `body`, `donationsAverage` and the donor `form` come
  only from `getDonationCampaign`.
* A campaign without a `goal` has no bar to fill — guard the division, as above.

### Campaign page with a donation form

Goal: show one campaign and let a visitor give an amount, with an optional message.

```tsx
// app/donate/[id]/page.tsx
import { createDonation, getDonationCampaign } from '@uidu/client';
import { RichText } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu'; // createClient({ workspace, publicToken, apiKey })

export default async function CampaignPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const campaign = await getDonationCampaign(uidu, {
    id: decodeURIComponent(id),
  });
  if (!campaign) notFound();
  const campaignId = campaign.id;

  async function donate(formData: FormData) {
    'use server';
    const message = String(formData.get('message') ?? '');
    const result = await createDonation(uidu, {
      input: {
        attributes: {
          donationCampaignId: campaignId,
          amount: Math.round(Number(formData.get('amount')) * 100), // euros in, cents out
          currency: 'EUR',
          contactAttributes: {
            email: String(formData.get('email')),
            contactableAttributes: {
              kind: 'person',
              firstName: String(formData.get('firstName')),
            },
          },
          body: message
            ? {
                type: 'doc',
                content: [
                  {
                    type: 'paragraph',
                    content: [{ type: 'text', text: message }],
                  },
                ],
              }
            : undefined,
        },
      },
    });
    if (result?.errors?.length) return; // return result.errors to the form
    // result?.donation?.id, result?.donation?.amount (cents) — now charge with your processor
  }

  return (
    <article>
      <h1>{campaign.name}</h1>
      {campaign.body && <RichText doc={campaign.body} />}
      <form action={donate}>
        <input name="amount" type="number" min="1" step="0.01" required />
        <input name="firstName" required />
        <input name="email" type="email" required />
        <textarea name="message" />
        <button>Donate</button>
      </form>
    </article>
  );
}
```

* **The donor.** `contactAttributes` creates the donor's contact in the same mutation — a new contact
  on every gift unless you pass the `id` of an existing one inside it (`contactAttributes: { id }`).
  `DonationAttributes` also has `contactId`.
* **Message and dedication** are JSON — typically a Tiptap document, as above.
* **The campaign's form.** When `campaign.form` is set, render it like any form (its questions are at
  `campaign.form.inputs`) and send the answers as
  `formResponseAttributes: { formId: campaign.form.id, fieldValuesAttributes: [...] }` — each
  `content` is `{ value }`, see [Forms](https://developers.uidu.org/docs/domains/forms.md).
* **Paying through uidu's checkout.** `createDonation`'s input also takes `orderAttributes` (a
  one-time payment) or `subscriptionAttributes` (recurring billing), next to `attributes`. With either,
  the donation's `amount` is computed from the order's or the plan's price, not from the `amount` you
  send. The attributes they expect depend on how checkout is configured in your workspace — see
  [Schema](https://developers.uidu.org/docs/reference/schema.md) for `OrderAttributes` and `SubscriptionAttributes`.
* The demo's version, with error handling, is `submitDonation` in `apps/demo/src/lib/actions.ts`.

### Launch a campaign from your server

Goal: create a fundraiser with a goal from a script or an admin tool.

```ts
import { createDonationCampaign } from '@uidu/client';
import { uidu } from '@/lib/uidu'; // must carry apiKey

const result = await createDonationCampaign(uidu, {
  input: { attributes: { name: 'Winter appeal', goal: 500000 } }, // €5,000.00 in cents
});

if (result?.errors?.length)
  throw new Error(result.errors.map((e) => e.name).join(', '));
result?.donationCampaign?.id;
```

* `DonationCampaignAttributes` also takes `body`, `abstract`, `cover`, `kind`, `category`, start and
  end dates and `publishedAt` — see [Schema](https://developers.uidu.org/docs/reference/schema.md). Change it later with
  `updateDonationCampaign`, remove it with `deleteDonationCampaign`.
* The same from a terminal: `uidu donations create | update | delete` — [CLI](https://developers.uidu.org/docs/tools/cli.md).

## API at a glance

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

| Function                                                                                                  | Kind  | Auth         | CLI                | Description                                                                                                                   |
| --------------------------------------------------------------------------------------------------------- | ----- | ------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| [`createDonation`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/donations.ts)         | write | Bearer       | —                  | Record a donation (amount in cents) to a campaign, creating the donor's contact inline; returns `{ errors, donation }` as-is. |
| [`createDonationCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/donations.ts) | write | Bearer       | `donations create` | Create a donation campaign (`goal` in cents); returns the payload as-is, `{ errors, donationCampaign }`.                      |
| [`deleteDonationCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `donations delete` | Delete a donation campaign by `{ input: { id } }`. Returns the payload (with `errors`) or null.                               |
| [`getDonationCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/donations.ts)    | read  | public token | `donations get`    | One donation campaign with its body, its totals (`goal`, `donationsAmount`, in cents) and its donor `form`, or null.          |
| [`listDonationCampaigns`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/donations.ts)  | read  | public token | `donations list`   | The workspace's donation campaigns as a flat array of summaries with their totals in cents; page with `first`/`after`.        |
| [`updateDonationCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `donations update` | Update a donation campaign: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                     |

**Types:** `DonationCampaign`

**CLI:**

| Entity      | list | get | create | update | delete |
| ----------- | :--: | :-: | :----: | :----: | :----: |
| `donations` |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |

---

# Events

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

> Fetch event listings, event details, and submit RSVPs (attendance) against the uidu Events API.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu events`, `uidu attendances`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

Events are what your workspace runs in time — a workshop, a gala, a weekly class. You list them
and show their details with the public token, and record RSVPs (**attendances**) from your server.

## Mental model

* **`Event`** — the metadata: `name`, `description`, `body`, `cover`, `published`, `publicPath` /
  `publicUrl`, `isPaidEvent`, `hasSeats`, `currentCapacity`, a `primaryAddress`
  (`formattedAddress`, `name`, `city`, `postalCode`, `countryCode`, `latitude`, `longitude`), and
  ready-made calendar links: `toGoogleCalendarUrl` and `toIcsUrl` (an `.ics` download).
* **`EventInstance`** — one occurrence in time: `id`, `beginsAt`, `finishesAt` (ISO timestamps). An
  event has one or more. `event.instance` is the **next or current** one — what most pages show —
  and `event.instances` is all of them, for a series view or a date picker. Only `getEvent` returns
  `instances` (and `instance.endDate` / `endTime`); `listEvents` summaries carry just `instance`.
* **`Attendance`** — a contact's RSVP, attached to an **instance, never to the event**: its
  `attendableId` is an `EventInstance.id`. An event with several dates has several instances, each
  attended independently. An attendance comes back with `id`, `checkedIn` and `contact.id`.

Shapes and gotchas:

* Passing the event id as `attendableId` is the classic mistake — always take it from
  `event.instance.id` or one of `event.instances[].id`.
* `beginsAt` / `finishesAt` are typed loosely (ISO8601 scalars come through as `any`); parse them with
  `new Date(...)` before comparing or formatting.
* `listEvents` has no date filter: it returns the workspace's events (`first`/`after` page through
  them, see [Pagination](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md)). Filter and sort on
  `instance.beginsAt` yourself.
* `createAttendance` and `createEvent` return the payload as-is — `{ errors, attendance }` and
  `{ errors, event }`, or null; validation failures come back in `errors`, not as a throw. Both need
  the `apiKey` client, so they run on your server or from the CLI (`uidu attendances create`,
  `uidu events create`), never with the public token in the browser. See
  [Writes](https://developers.uidu.org/docs/foundations/writes.md) and [Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## Use cases

### Upcoming events page

Goal: a page listing the events that haven't started yet, soonest first.

```tsx
// app/events/page.tsx — a Server Component
import Link from 'next/link';
import { listEvents } from '@uidu/client';
import { uidu } from '@/lib/uidu';

export default async function EventsPage() {
  const events = await listEvents(uidu, { first: 50 });
  const now = Date.now();

  const upcoming = events
    .filter(
      (e) =>
        e.instance?.beginsAt && new Date(e.instance.beginsAt).getTime() > now,
    )
    .sort(
      (a, b) =>
        new Date(a.instance!.beginsAt).getTime() -
        new Date(b.instance!.beginsAt).getTime(),
    );

  return (
    <ul>
      {upcoming.map((event) => (
        <li key={event.id}>
          <Link href={`/event/${encodeURIComponent(event.id)}`}>
            {event.name}
          </Link>
          <time>{new Date(event.instance!.beginsAt).toLocaleDateString()}</time>
          {event.primaryAddress?.city && (
            <span> · {event.primaryAddress.city}</span>
          )}
        </li>
      ))}
    </ul>
  );
}
```

* Each summary has `id`, `name`, `description`, `cover`, `published`, `publicPath`, `isPaidEvent`,
  `primaryAddress` (`formattedAddress`, `city`) and `instance` (`id`, `beginsAt`, `finishesAt`). An
  event with no upcoming occurrence has no `instance` to show.
* Put the detail route under a path segment (`/event/[id]`), not a root `/[id]` that would also catch
  `/favicon.ico`.
* Need every event, for a sitemap or `generateStaticParams`? Use `paginate` with
  `ListEventsDocument` — [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).

### Event page with RSVP

Goal: show one event with its dates and calendar links, and let a visitor RSVP for a date.

```tsx
// app/event/[id]/page.tsx
import { createAttendance, getEvent } from '@uidu/client';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu'; // createClient({ workspace, publicToken, apiKey })

export default async function EventPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const event = await getEvent(uidu, { id: decodeURIComponent(id) });
  if (!event) notFound();

  async function rsvp(formData: FormData) {
    'use server';
    const result = await createAttendance(uidu, {
      input: {
        attributes: {
          attendableId: String(formData.get('instanceId')), // an INSTANCE id, not event.id
          contactAttributes: {
            email: String(formData.get('email')),
            contactableAttributes: {
              kind: 'person',
              firstName: String(formData.get('firstName')),
            },
          },
        },
      },
    });
    if (result?.errors?.length) {
      // validation failure: return result.errors to the form
      return;
    }
    // result?.attendance?.id, result?.attendance?.contact?.id
  }

  return (
    <article>
      <h1>{event.name}</h1>
      <p>{event.primaryAddress?.formattedAddress}</p>
      <a href={event.toGoogleCalendarUrl ?? undefined}>
        Add to Google Calendar
      </a>
      <a href={event.toIcsUrl ?? undefined}>Download .ics</a>

      <form action={rsvp}>
        <select name="instanceId" defaultValue={event.instance?.id}>
          {(event.instances ?? []).map((i) => (
            <option key={i.id} value={i.id}>
              {new Date(i.beginsAt).toLocaleString()}
            </option>
          ))}
        </select>
        <input name="firstName" required />
        <input name="email" type="email" required />
        <button>RSVP</button>
      </form>
    </article>
  );
}
```

* For a single-date event, skip the picker and send `event.instance.id`.
* `AttendanceAttributes` beyond the instance: `contactAttributes` creates the attendee in the same
  mutation, or `contactId` attaches an existing contact instead; `formResponseAttributes` carries
  extra answers (dietary needs…) in the shape described in [Forms](https://developers.uidu.org/docs/domains/forms.md) — every
  `content` is `{ value }`; `rsvpStatus` is `pending`, `accepted`, `declined` or `tentative`.
* `isPaidEvent`, `hasSeats` and `currentCapacity` tell you whether a plain RSVP fits the event;
  paid tickets go through uidu's checkout, which this domain doesn't wrap.
* The demo's version, with error handling, is `submitRsvp` in `apps/demo/src/lib/actions.ts`.

### Publish an event from your server

Goal: create an event with its first date from a script or an admin tool.

```ts
import { createEvent, updateEvent } from '@uidu/client';
import { uidu } from '@/lib/uidu'; // must carry apiKey

const created = await createEvent(uidu, {
  input: {
    attributes: {
      name: 'Launch party',
      eventInstancesAttributes: [
        {
          startDate: '2026-11-20',
          startTime: '19:00',
          endDate: '2026-11-20',
          endTime: '23:00',
        },
      ],
    },
  },
});
if (created?.errors?.length)
  throw new Error(created.errors.map((e) => e.name).join(', '));

const id = created?.event?.id;
if (id)
  await updateEvent(uidu, { input: { id, attributes: { hasSeats: true } } });
```

* Dates are written per instance as `startDate` / `endDate` (`YYYY-MM-DD`) and `startTime` /
  `endTime` (`HH:MM`); an instance without `endTime` lasts one hour. Reads give you the combined
  `beginsAt` / `finishesAt` instead.
* `EventAttributes` also takes `body` (rich text), `cover`, `addressesAttributes`, `eventTagList`,
  `preferences` — the full list is in [Schema](https://developers.uidu.org/docs/reference/schema.md). The payload returns the new
  event's `id` and `name`.
* The same from a terminal: `uidu events create`, `uidu events update` — [CLI](https://developers.uidu.org/docs/tools/cli.md).
  Events can't be deleted through the SDK or the CLI.

## API at a glance

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

| Function                                                                                         | Kind  | Auth         | CLI                  | Description                                                                                                                   |
| ------------------------------------------------------------------------------------------------ | ----- | ------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [`createAttendance`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/events.ts) | write | Bearer       | `attendances create` | RSVP a contact to an event: `attendableId` is an event *instance* id, never the event id.                                     |
| [`createEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/events.ts)      | write | Bearer       | `events create`      | Create an event; returns the payload as-is, `{ errors, event }`, validation failures included.                                |
| [`getEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/events.ts)         | read  | public token | `events get`         | One event with its primary address, its next or current `instance` and all its `instances`, or null.                          |
| [`listEvents`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/events.ts)       | read  | public token | `events list`        | The workspace's events as a flat array of summaries, each with its next or current `instance`; page with `first`/`after`.     |
| [`updateEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)   | write | Bearer       | `events update`      | Update an event: `{ input: { id, attributes } }` (create it with `createEvent`). Returns the payload (with `errors`) or null. |

**Types:** `Event`

**CLI:**

| Entity        | list | get | create | update | delete |
| ------------- | :--: | :-: | :----: | :----: | :----: |
| `events`      |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
| `attendances` |   —  |  —  |    ✓   |    —   |    —   |

---

# Forms

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

> Fetch form schemas and submit form responses against the uidu Forms API.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu forms`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

A form is a questionnaire you build in the uidu workspace: you read its schema to render it, and
submit the answers as a **form response**. Forms stand alone or ride along with other records — a CMS
block, a call, a donation campaign, an event RSVP — and the same rules apply everywhere.

## Mental model

* **`Form`** — `id`, `name&#x60;, and its questions under &#x2A;*`inputs`**, a Relay connection of
  `FormQuestion` nodes (`formQuestions` in the schema, aliased to `inputs` by the query; same data).
* **`FormQuestion`** — how the question is asked on this form: `label`, `required`, `hidden`,
  `placeholder`, `hint`, `position`, `fieldKind`, `rules`, `preferences&#x60;, and its &#x2A;*`field`**.
* **`Field`** — what the answer is: `id` (the `fieldId` you submit against), `kind` (`text`,
  `select`, `attachments`, …), `shortname`, `type` (`'system'` for built-in ones), `options` (the
  `{ id, name }` choices of select-like fields, `optionList` in the schema) and, for media fields,
  the upload target (`uploadPresignedUrl`, `uploadUrl`, `uploadParams`).
* **`FormResponse`** — one submission: `fieldValuesAttributes` (one `{ fieldId, content }` per answer),
  an optional respondent created inline with `contactAttributes` (or an existing `contactId`), and a
  `status`. It comes back with `id`, `status`, `fieldValues` (`id`, `content`, `linkedRecordId`,
  `field.id`) and `contact.id`.
  Inside a [custom app](https://developers.uidu.org/docs/paths/custom-app.md) the response belongs to the signed-in member, so
  `contactId` and `contactAttributes` aren't accepted there.

Where forms appear besides `getForm`: a CMS block's field value links one (`linkedRecord`), a call
and a donation campaign carry a `form`, and RSVPs, donations and applications take the answers
nested as `formResponseAttributes`.

Shapes and gotchas for the whole domain:

* **`content` is always `{ value: … }`** — `{ value: 'text' }`, `{ value: 42 }`, `{ value: true }`,
  `{ value: '2026-07-27' }`, `{ value: ['gid://…'] }` for `multipleSelect`, `{ value: doc }` for
  `richText`/`json`. A bare value isn't rejected: the record saves, its typed columns stay empty and
  later reads of that field error. `UiduClient.mutate` wraps a bare `content` for you and warns in
  development; other API clients won't. The full rule:
  [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).
* **Status follows one event.** A response is `initialized` until you send `event: 'complete!'`, on
  create or on a later update; then it is `completed` (`abandoned` is the third state). Omit `event`
  and the response is saved as a draft. `'complete!'` is the only event the mutations act on, and it
  fires once: completing an already completed response is a no-op. Completion is what announces the
  response to the workspace — except for a form attached to another record (a call, a campaign…),
  where that record's own flow announces it.
* **Answers to selects** are option `id`s from `field.options`.
* **Errors come back, they don't throw.** Both mutations return the payload as-is,
  `{ errors, formResponse } | null`; a non-empty `errors` (`{ key, name, type }`) is a validation
  failure. See [Writes](https://developers.uidu.org/docs/foundations/writes.md) and [Errors](https://developers.uidu.org/docs/foundations/errors.md).
* **Reads are public, writes are not.** `getForm` and `listForms` work with the public token, in the
  browser too. `createFormResponse` and `updateFormResponse` need the `apiKey` client, so they run in a
  server action — never with the public token from the browser. See
  [Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## Use cases

### A contact form on your site

Goal: render a form built in uidu and store each submission as a completed response.

```tsx
// app/contact/page.tsx — a Server Component
import { createFormResponse, getForm } from '@uidu/client';
import {
  DynamicForm,
  type DynamicFormResult,
  type DynamicFormValues,
} from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu'; // createClient({ workspace, publicToken, apiKey })

const FORM_ID = 'Z2lkOi8v…'; // from the Forms app, or `uidu forms list`

export default async function ContactPage() {
  const form = await getForm(uidu, { id: FORM_ID });
  if (!form) notFound();

  async function submit(values: DynamicFormValues): Promise<DynamicFormResult> {
    'use server';
    const result = await createFormResponse(uidu, {
      input: {
        attributes: {
          formId: FORM_ID,
          fieldValuesAttributes: values.fieldValues.map((fv) => ({
            fieldId: fv.fieldId,
            content: { value: fv.value },
          })),
          contactAttributes: values.contact && {
            email: values.contact.email,
            contactableAttributes: {
              kind: 'person',
              firstName: values.contact.firstName,
              lastName: values.contact.lastName || undefined,
            },
          },
        },
        event: 'complete!', // omit to save a draft
      },
    });

    if (result?.errors?.length) return { ok: false, errors: result.errors };
    return { ok: true, meta: { responseId: result?.formResponse?.id } };
  }

  return <DynamicForm form={form} action={submit} submitLabel="Send" />;
}
```

* [`<DynamicForm>`](https://developers.uidu.org/docs/tools/react/components.md#dynamicform-form-action-) walks `form.inputs`, picks
  an HTML control per `fieldKind` (a `<select>` when the field has `options`), skips hidden and system
  questions, adds first name / last name / email unless `includeContactFields={false}`, and calls your
  action with `{ contact, fieldValues, formData }`. Every value it collects is a string — a checkbox
  arrives as `'true'`/`'false'`. It has no file input: see [Accept a file upload](https://developers.uidu.org/docs/domains/forms.md#accept-a-file-upload).
* To render the questions yourself, walk the same connection:
  `form.inputs.edges?.map((e) => e?.node)` gives each question's `label`, `required`, `field.kind`
  and `field.options`.
* A forms index: `listForms(uidu, { first: 50 })` returns summaries — `id`, `name`, `cover`,
  `published`, `publishedAt` — without the questions; filter on `published` and call `getForm` for the
  one you render.

### Multi-step form with drafts

Goal: save each step as the visitor goes, so a long form survives a closed tab, and complete it on the
last step.

```ts
// app/apply/actions.ts
'use server';

import { createFormResponse, updateFormResponse } from '@uidu/client';
import { uidu } from '@/lib/uidu';

type Answer = { fieldId: string; value: unknown };

export async function saveStep(
  formId: string,
  responseId: string | null,
  answers: Answer[],
  last: boolean,
) {
  const event = last ? 'complete!' : undefined; // no event = still a draft

  if (!responseId) {
    const created = await createFormResponse(uidu, {
      input: {
        attributes: {
          formId,
          fieldValuesAttributes: answers.map((a) => ({
            fieldId: a.fieldId,
            content: { value: a.value },
          })),
        },
        event,
      },
    });
    if (created?.errors?.length)
      return { ok: false as const, errors: created.errors };
    return { ok: true as const, responseId: created?.formResponse?.id ?? null };
  }

  // Answers already saved are updated by their field value `id`; new ones are added without one.
  const updated = await updateFormResponse(uidu, {
    input: {
      id: responseId,
      event,
      attributes: {
        formId,
        fieldValuesAttributes: answers.map((a) => ({
          fieldId: a.fieldId,
          content: { value: a.value },
        })),
      },
    },
  });
  if (updated?.errors?.length)
    return { ok: false as const, errors: updated.errors };
  return { ok: true as const, responseId };
}
```

* Keep the returned `responseId` between steps (a cookie, the URL, client state) and pass it back.
* To **change** an answer you already saved, include that field value's `id` in its entry:
  `{ id, fieldId, content: { value } }`. Without an `id` the entry adds a new value. The ids are in
  every payload — `formResponse.fieldValues[]` pairs each `id` with its `field.id`.
* `formResponse.status` tells you where it stands: `initialized` while it's a draft, `completed`
  after `'complete!'`.
* Validate per step on the client for a fast UI, but treat `errors` from the server as the answer.

### Accept a file upload

Goal: let the visitor attach a file (a CV, a photo) to an `attachments`-kind question.

```ts
// 1. In the browser: upload the file straight to storage with the field's presigned POST.
//    Pass `question.field.uploadUrl` and `question.field.uploadParams` from a node of
//    form.inputs (read with the public token).
async function uploadToField(
  uploadUrl: string,
  uploadParams: Record<string, string>,
  file: File,
) {
  const body = new FormData();
  Object.entries(uploadParams).forEach(([key, value]) =>
    body.append(key, value),
  );
  body.append('file', file);

  const res = await fetch(uploadUrl, { method: 'POST', body });
  if (!res.ok) throw new Error('upload failed');

  // The uploaded file as uidu's attachment data: the object key without its `cache/` prefix.
  return JSON.stringify({
    id: uploadParams.key.replace(/^cache\//, ''),
    storage: 'cache',
    metadata: { filename: file.name, mime_type: file.type, size: file.size },
  });
}

// 2. In a server action: submit the response with the file as a linked Attachment.
await createFormResponse(uidu, {
  input: {
    attributes: {
      formId,
      fieldValuesAttributes: [
        {
          fieldId: question.field.id,
          linkedRecordType: 'Attachment',
          linkedRecordAttributes: { file: uploadedFile }, // the string from step 1
        },
      ],
    },
    event: 'complete!',
  },
});
```

* `field.uploadPresignedUrl` is the whole presign as JSON (`{ url, fields }`); `uploadUrl` and
  `uploadParams` are its two halves. Each read of the form signs a fresh, short-lived target, and only
  media fields have one — so read the form close to the upload.
* `'Attachment'` is the only `linkedRecordType` the API accepts on a field value. An `attachments`
  answer lives in the linked record, not in `content`.
* The upload itself carries no uidu token; the response that references it is still a write, from
  the server. The same flow serves a call's application form: its questions are at
  `call.form.inputs.edges[].node`. A fuller walk-through: [File upload through Forms](https://developers.uidu.org/docs/recipes/file-upload.md).

## API at a glance

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

| Function                                                                                          | Kind  | Auth         | CLI            | Description                                                                                                                                              |
| ------------------------------------------------------------------------------------------------- | ----- | ------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createForm`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)     | write | Bearer       | `forms create` | Create a form, questions included via `attributes.formQuestionsAttributes`. Returns the payload (with `errors`) or null. The new form is at `form.node`. |
| [`createFormResponse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/forms.ts) | write | Bearer       | —              | Submit a new response to a form; returns the payload as-is, `{ errors, formResponse }`, validation failures included.                                    |
| [`deleteForm`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)     | write | Bearer       | `forms delete` | Delete a form by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                       |
| [`getForm`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/forms.ts)            | read  | public token | `forms get`    | One form with its questions (`inputs`, a connection of `FormQuestion` nodes with field metadata) ready to render, or null.                               |
| [`listForms`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/forms.ts)          | read  | public token | `forms list`   | The workspace's forms as a flat array of summaries, without their questions (use `getForm` for those); page with `first`/`after`.                        |
| [`updateForm`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)     | write | Bearer       | `forms update` | Update a form: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                             |
| [`updateFormResponse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/forms.ts) | write | Bearer       | —              | Update a form response (answers by field value `id`, or a state `event` like `'complete!'`); returns `{ errors, formResponse }` as-is.                   |

**Types:** `Form`

**CLI:**

| Entity  | list | get | create | update | delete |
| ------- | :--: | :-: | :----: | :----: | :----: |
| `forms` |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |

---

# 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` |   ✓  |  —  |    —   |    —   |    —   |

---

# Help Center

URL: https://developers.uidu.org/docs/domains/help-center

> Fetch help center channels, KB collections, and articles.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu channel`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

The help center is a workspace's self-service support site: **channels** of knowledge-base
articles, grouped by topic. `getChannel` reads a whole channel — its collections and their
articles — in one request, which is usually all a help-center front end needs.

## Mental model

```
Workspace
└── Channel                 { id, name, isDefault } — the help center itself
    ├── kbCollections       topics: { id, name, description, iconData }
    │   └── kbArticles      { id, name, body, updatedAt, workspaceTagList }
    └── channelKbCollections   { position, kbCollection { id } } — the order inside this channel
```

* **Which channel.** A workspace can have several. `listChannels` marks the main one with
  `isDefault`: code that has no channel id configured should prefer it over the first row.
* **One request, capped.** `getChannel` fetches the **last 100 collections** and the **last 100
  articles** of each. Past that, read collections and articles directly with the
  [Knowledge base](https://developers.uidu.org/docs/domains/kb.md) functions, which paginate.
* **Order.** Articles inside a collection come back ordered by their `position`. A collection's
  position in *this* channel lives in `channelKbCollections` (by `kbCollection.id`), not on the
  collection — sort with it when the order matters.
* **Drafts are left out** server-side: every article you get here is published.
* **Field shapes.** A collection's `name` and `description` are JSON — usually a string, possibly a
  localization map like `{ en, it }`; `iconData` is icon metadata (JSON). An article's `name` is a
  string, its `body` is rich-text JSON (render it with
  [`<RichText>`](https://developers.uidu.org/docs/tools/react/components.md)), and `workspaceTagList` is its tags,
  `{ id, name, color }`.
* **Search.** Knowledge-base articles aren't in the workspace search index, so
  [`search`](https://developers.uidu.org/docs/domains/search.md) won't find them: filter the channel's articles in memory.
* **Writes.** `createChannel`, `updateChannel` and `deleteChannel` need the Bearer client and return
  their payload with `errors` (the new channel is at `channel.node`). Collections and articles are
  written through the [Knowledge base](https://developers.uidu.org/docs/domains/kb.md) functions.

## Use cases

### Help center home

Goal: a `/help` page that shows the default channel's topics in the channel's own order, each with
its articles.

```tsx
// src/lib/help.ts
import { cache } from 'react';
import { getChannel, listChannels } from '@uidu/client';
import { uidu } from './uidu';

export const getHelpChannel = cache(async () => {
  const channels = await listChannels(uidu, { first: 25 });
  const main = channels.find((c) => c.isDefault) ?? channels[0];
  return main ? getChannel(uidu, { id: main.id }) : null;
});

// A collection's name/description may be a plain string or a { en, it, … } map.
export function label(value: unknown, locale = 'en'): string {
  if (typeof value === 'string') return value;
  if (value && typeof value === 'object') {
    const map = value as Record<string, unknown>;
    return String(map[locale] ?? Object.values(map)[0] ?? '');
  }
  return '';
}

// src/app/help/page.tsx
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getHelpChannel, label } from '@/lib/help';

export default async function HelpHomePage() {
  const channel = await getHelpChannel();
  if (!channel) notFound();

  const position = new Map(
    (channel.channelKbCollections?.edges ?? []).flatMap((e) =>
      e?.node ? [[e.node.kbCollection.id, e.node.position] as const] : [],
    ),
  );
  const collections = (channel.kbCollections?.edges ?? [])
    .flatMap((e) => (e?.node ? [e.node] : []))
    .sort((a, b) => (position.get(a.id) ?? 0) - (position.get(b.id) ?? 0));

  return (
    <div>
      <h1>{channel.name}</h1>
      {collections.map((c) => (
        <section key={c.id}>
          <h2>{label(c.name)}</h2>
          <p>{label(c.description)}</p>
          <ul>
            {(c.kbArticles?.edges ?? []).map(
              (a) =>
                a?.node && (
                  <li key={a.node.id}>
                    <Link href={`/help/${encodeURIComponent(a.node.id)}`}>{a.node.name}</Link>
                  </li>
                ),
            )}
          </ul>
        </section>
      ))}
    </div>
  );
}
```

* Set the channel id in an env var instead of `listChannels` if you want to pin one: ids are
  GlobalIDs, so they belong to one workspace and don't travel between workspaces.
* `cache()` lets the home and the article page share one `getChannel` per render.

### Article page

Goal: `/help/[articleId]` renders one article with its body, under its topic.

```tsx
// src/app/help/[articleId]/page.tsx
import { notFound } from 'next/navigation';
import { getKbArticle } from '@uidu/client';
import { RichText } from '@uidu/react';
import { label } from '@/lib/help';
import { uidu } from '@/lib/uidu';

export default async function ArticlePage({
  params,
}: {
  params: Promise<{ articleId: string }>;
}) {
  const { articleId } = await params;
  const article = await getKbArticle(uidu, { id: decodeURIComponent(articleId) });
  if (!article) notFound();

  return (
    <article>
      {article.kbCollection && <p>{label(article.kbCollection.name)}</p>}
      <h1>{article.name}</h1>
      <RichText doc={article.body} />
      <small>Updated {new Date(article.updatedAt as string).toLocaleDateString()}</small>
    </article>
  );
}
```

* `getKbArticle` fetches the one article instead of the whole channel. If you already hold the
  channel (from `getHelpChannel()`), the article's `body` is in it too — look it up there and save
  a request.
* `getKbArticle` doesn't check which channel the article belongs to. If you run several channels
  and must keep them apart, find the article inside the channel instead.

### Filter articles on the page

Goal: a quick filter over the help center, by text or by tag, without a server round trip.

```tsx
// src/app/help/article-filter.tsx
'use client';
import { useState } from 'react';
import Link from 'next/link';

type Item = { id: string; name: string | null; tags: string[] };

export function ArticleFilter({ items }: { items: Item[] }) {
  const [q, setQ] = useState('');
  const needle = q.trim().toLowerCase();
  const hits = needle
    ? items.filter(
        (a) =>
          a.name?.toLowerCase().includes(needle) ||
          a.tags.some((t) => t.toLowerCase().includes(needle)),
      )
    : items;

  return (
    <>
      <input
        type="search"
        value={q}
        onChange={(e) => setQ(e.target.value)}
        aria-label="Filter articles"
      />
      <ul>
        {hits.map((a) => (
          <li key={a.id}>
            <Link href={`/help/${encodeURIComponent(a.id)}`}>{a.name}</Link>
          </li>
        ))}
      </ul>
    </>
  );
}

// In the Server Component, flatten the channel into plain props:
const items = collections.flatMap((c) =>
  (c.kbArticles?.edges ?? []).flatMap((e) => {
    const a = e?.node;
    if (!a) return [];
    const tags = (a.workspaceTagList ?? []).flatMap((t) => (t.name ? [t.name] : []));
    return [{ id: a.id, name: a.name ?? null, tags }];
  }),
);
// <ArticleFilter items={items} />
```

* Pass only what the filter needs: the article `body` is large and doesn't need to reach the
  browser.

## API at a glance

Every `@uidu/client` function in **Help center**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#help-center) has the rest of the SDK.

| Function                                                                                          | Kind  | Auth         | CLI              | Description                                                                                                                                                                                |
| ------------------------------------------------------------------------------------------------- | ----- | ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`createChannel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `channel create` | Create a help-center channel. Returns the payload (with `errors`) or null. The new channel is at `channel.node`.                                                                           |
| [`deleteChannel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `channel delete` | Delete a help-center channel by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                          |
| [`getChannel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/help-center.ts)   | read  | public token | `channel get`    | One help center channel with its collections and their articles in a single request (the last 100 of each), or null.                                                                       |
| [`listChannels`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/help-center.ts) | read  | public token | `channel list`   | The workspace's help center channels. `isDefault` marks the one the workspace treats as its main channel — a caller that has no channel id configured should prefer it over the first row. |
| [`updateChannel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `channel update` | Update a help-center channel: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                |

**Types:** `Channel`

**CLI:**

| Entity    | list | get | create | update | delete |
| --------- | :--: | :-: | :----: | :----: | :----: |
| `channel` |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |

---

# Jobs & applications

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

> List job postings, render a careers page, and let candidates apply — from the apply flow to the private applications board.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser; Bearer for `listApplications`
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu jobs`, `uidu applications`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

A **job** is a [call](https://developers.uidu.org/docs/domains/calls.md) whose `kind` is `job`; an **application** is a
candidate's answer to it: a contact plus their form response, linked to the job. Together they
cover a careers site: a listing, a detail page with salary and location, an apply form, and the
board of applications received.

## Mental model

```
Call (kind: 'job')          ← listJobs / getJob — a filtered view over calls
  ├─ salaryMin / salaryMax, quantity, expiresAt, primaryAddress
  ├─ form                   ← the application form (questions)
  └─→ Application           ← createApplication / listApplications
        ├─ contact          the candidate
        ├─ formResponse     their answers
        ├─ status           pipeline stage on the board
        └─ aasmState        'draft' → 'completed' (completeApplication) → 'accepted' | 'declined'
```

* **Jobs are a derived view.** There is no `jobs` field in the GraphQL schema: `listJobs` and
  `getJob` call the [calls](https://developers.uidu.org/docs/domains/calls.md) queries and keep only `kind: 'job'`. `Job` is
  the `Call` type, so everything on the Calls page — summary vs full fields, the inlined
  `form`, salaries in whole units — holds here.
* **`listJobs` filters after fetching.** `first` is a page of **calls**; the `kind` filter runs
  on what came back. `listJobs(client, { first: 20 })` can return fewer than 20 jobs when the
  workspace also has other calls.
* **`getJob` returns `null` for a call of another kind**, not only for an unknown id, so a
  careers route can't be used to show a grant call.
* **Applications are first-class**: their own GraphQL type and operations, unlike jobs.
* **A new application is a draft.** `createApplication` leaves it in `draft`, and the call's
  board lists only completed ones: until `completeApplication` runs, nobody sees the candidate.
* **Auth.** Reading jobs works with a public token. Applying is a write: `createApplication`
  and `completeApplication` run in a server action with the `apiKey` client, never from the
  browser. `listApplications` needs the Bearer too: applications hold private candidate data.
  See [Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## Use cases

The examples import `uidu` from `lib/uidu.ts`, a client built with `publicToken` and `apiKey`
and imported only from server code, as in [Authentication](https://developers.uidu.org/docs/foundations/authentication.md#bearer--the-workspace).

### Careers page

Goal: list the open positions with their salary range and city.

```tsx
// app/careers/page.tsx
import { listJobs } from '@uidu/client';
import { formatSalaryRange } from '@uidu/react';
import { uidu } from '@/lib/uidu';

export default async function CareersPage() {
  // A page of *calls*, filtered to jobs afterwards: ask for more than you show.
  const jobs = await listJobs(uidu, { first: 100 });

  return (
    <ul>
      {jobs.map((job) => {
        const salary = formatSalaryRange(job, {
          currency: 'EUR',
          locale: 'it-IT',
        });
        return (
          <li key={job.id}>
            <a href={`/careers/${encodeURIComponent(job.id)}`}>{job.name}</a>
            {job.primaryAddress?.city && ` · ${job.primaryAddress.city}`}
            {salary && ` · ${salary}`}
          </li>
        );
      })}
    </ul>
  );
}
```

`formatSalaryRange(job, { currency?, locale? })` from `@uidu/react` is a pure function, safe in
Server and Client Components. Defaults: `currency: 'EUR'` (ISO 4217), `locale: 'en-US'`
(BCP 47). It never shows decimals, and returns `string | null`:

```ts
formatSalaryRange({ salaryMin: 30000, salaryMax: 40000 }); // '€30,000–€40,000'
formatSalaryRange({ salaryMin: 30000, salaryMax: 30000 }); // '€30,000'
formatSalaryRange({ salaryMin: 30000, salaryMax: null }); // 'From €30,000'
formatSalaryRange({ salaryMin: null, salaryMax: 40000 }); // 'Up to €40,000'
formatSalaryRange({ salaryMin: null, salaryMax: null }); // null — hide the field
```

The call's own currency isn't in the SDK's selection, so pass `currency` when it isn't euros.

### Job page with an apply form

Goal: one job's page, and an apply form that records an application and submits it, so it
reaches the board.

```tsx
// app/careers/[id]/page.tsx
import { getJob, createApplication, completeApplication } from '@uidu/client';
import {
  DynamicForm,
  RichText,
  formatSalaryRange,
  type DynamicFormValues,
} from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

export default async function JobPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const job = await getJob(uidu, { id: decodeURIComponent(id) });
  if (!job) notFound(); // unknown id, or a call that isn't a job

  const jobId = job.id;
  const formId = job.form?.id;

  async function apply(values: DynamicFormValues) {
    'use server';
    const created = await createApplication(uidu, {
      input: {
        attributes: {
          applicableId: jobId,
          contactAttributes: values.contact && {
            email: values.contact.email,
            contactableAttributes: {
              kind: 'person',
              firstName: values.contact.firstName,
              lastName: values.contact.lastName || undefined,
            },
          },
          formResponseAttributes: formId
            ? {
                formId,
                fieldValuesAttributes: values.fieldValues.map((fv) => ({
                  fieldId: fv.fieldId,
                  content: { value: fv.value }, // always wrapped
                })),
              }
            : undefined,
        },
      },
    });
    if (created?.errors?.length)
      return { ok: false as const, errors: created.errors };

    const applicationId = created?.application?.id;
    if (!applicationId)
      return {
        ok: false as const,
        errors: [{ name: 'No application created' }],
      };

    // Without this the application stays a draft and never reaches the board.
    const completed = await completeApplication(uidu, applicationId);
    if (completed?.errors?.length)
      return { ok: false as const, errors: completed.errors };

    return { ok: true as const, meta: { applicationId } };
  }

  const salary = formatSalaryRange(job, { currency: 'EUR', locale: 'it-IT' });

  return (
    <article>
      <h1>{job.name}</h1>
      {salary && <p>{salary}</p>}
      <p>{job.primaryAddress?.formattedAddress}</p>
      {job.body && <RichText doc={job.body} />}
      {job.form && (
        <DynamicForm form={job.form} action={apply} submitLabel="Apply" />
      )}
    </article>
  );
}
```

* **`createApplication`** links the candidate and their answers to the job (`applicableId`) in
  one mutation. The candidate is `contactAttributes` (a new or matched contact) or `contactId`
  (an existing one); the answers are `formResponseAttributes`. Its payload is
  `{ errors, application }`: `application` has `id`, `aasmState`, `summary`, `completedAt`,
  `createdAt`, `contact { id, humanName, email }`, `applicable { id, name, kind }` and
  `formResponse { id }`. It doesn't throw on validation errors: check `errors`.
* **`completeApplication(client, id)`** fires the `complete!` event: it stamps `completedAt`,
  marks the form response completed and notifies whoever follows the call, the same call uidu's
  own public site makes after its last step. It's a no-op on an application already completed.
  Its payload's `application` has `id`, `aasmState`, `completedAt` and `formResponse { id }`.
* **Multi-step flows** use `updateApplication(client, { input: { id, attributes, event? } })`
  to save answers as the candidate goes, then complete. `attributes` is required (pass `{}` when
  you only fire an event), and `event` sits on `input`, not inside `attributes`;
  `completeApplication` is `updateApplication` with `event: 'complete!'` and `attributes: {}`.
* **Attachments** (CV, portfolio) go through the [file upload flow](https://developers.uidu.org/docs/domains/forms.md#file-uploads).
* A transport failure throws a `UiduError` instead — see [Errors](https://developers.uidu.org/docs/foundations/errors.md).

### Applications board

Goal: an internal page listing the applications received, with the candidate, the job and the
pipeline stage.

```tsx
// app/admin/applications/page.tsx — server only, needs the apiKey client
import { listApplications } from '@uidu/client';
import { uidu } from '@/lib/uidu';

export default async function ApplicationsPage() {
  const applications = await listApplications(uidu, { first: 50 });

  return (
    <table>
      <tbody>
        {applications.map((a) => (
          <tr key={a.id}>
            <td>
              {a.contact?.humanName} · {a.contact?.email}
            </td>
            <td>{a.applicable?.__typename === 'Call' && a.applicable.name}</td>
            <td>{a.status?.name ?? a.aasmState}</td>
            <td>
              {a.completedAt &&
                new Date(a.completedAt as string).toLocaleDateString()}
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
```

* `listApplications` covers the whole workspace, every job. Filter on `applicable.id` for one
  job's board.
* `aasmState` is the lifecycle (`draft`, `completed`, `accepted`, `declined`); `status`
  (`id`, `name`, `color`) is the stage on the board.
* With a public-token client the list comes back empty rather than failing — check that the
  client has `apiKey`. To read every application, pass `ListApplicationsDocument` to `paginate`
  ([Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md)).

## API at a glance

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

| Function                                                                                                  | Kind  | Auth         | CLI                   | Description                                                                                                                                                                                                               |
| --------------------------------------------------------------------------------------------------------- | ----- | ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`completeApplication`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/applications.ts) | write | Bearer       | —                     | Submit a draft application, the last step of applying.                                                                                                                                                                    |
| [`createApplication`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/applications.ts)   | write | Bearer       | `applications create` | Submit an application to a job. Pass `attributes.applicableId` (the job's id) plus `contactAttributes` and/or `formResponseAttributes`. Returns the mutation payload as-is (including any `errors`) rather than throwing. |
| [`getJob`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/jobs.ts)                      | read  | public token | `jobs get`            | Fetch a single job posting; returns `null` if the call is not a job.                                                                                                                                                      |
| [`listApplications`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/applications.ts)    | read  | Bearer       | `applications list`   | List applications across the workspace (requires a Bearer token).                                                                                                                                                         |
| [`listJobs`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/jobs.ts)                    | read  | public token | `jobs list`           | List job postings (calls of kind `job`).                                                                                                                                                                                  |
| [`updateApplication`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/applications.ts)   | write | Bearer       | —                     | Update an application, optionally firing a state event (`event: 'complete!'`). Returns the mutation payload as-is (including any `errors`) rather than throwing.                                                          |

**Types:** `Application`, `Job`

**CLI:**

| Entity         | list | get | create | update | delete |
| -------------- | :--: | :-: | :----: | :----: | :----: |
| `jobs`         |   ✓  |  ✓  |    —   |    —   |    —   |
| `applications` |   ✓  |  —  |    ✓   |    —   |    —   |

---

# Knowledge base (direct)

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

> Fetch KB collections and articles directly, outside the help-center channel scope.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu kb-collections`, `uidu kb-articles`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

The knowledge base is the content behind the [Help Center](https://developers.uidu.org/docs/domains/help-center.md):
**collections** of **articles**. The functions on this page read and write them at the workspace
level, with no channel in between — use them to list every collection across channels, to fetch an
article without knowing its collection, or to go past the 100-per-level cap of `getChannel`.

## Mental model

```
Workspace
├── KbCollection     { id, name, description, iconData, position, createdAt, updatedAt }
│   └── KbArticle    { id, name, body, position, isDraft, createdAt, updatedAt, kbCollection { id, name } }
└── Channel          a help center: shows some collections, in its own order (see Help Center)
```

* **Lists are flat arrays**, one Relay page at a time: `listKbCollections` and `listKbArticles` take
  `first`/`after` (or `last`/`before`). For all of them, `paginate` with `ListKbCollectionsDocument`
  or `ListKbArticlesDocument` — see [Pagination](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).
* **`listKbArticles` spans every collection** and returns summaries — `name`, `position`, `isDraft`,
  `kbCollection { id, name }`, timestamps — &#x2A;*without `body`**. Only `getKbArticle` returns the body.
* **`getKbCollection` embeds its first 50 articles** (`id`, `name`, `position`, `isDraft`,
  `updatedAt`) with their `pageInfo`, ordered by `position`. A bigger collection needs
  `listKbArticles`, filtered on `kbCollection.id`.
* **Drafts are left out** of the article lists server-side, so `isDraft` is `false` on everything a
  list returns.
* **Field shapes.** A collection's `name` and `description` are JSON — often a string, possibly a
  localization map like `{ en, it }`; `iconData` is JSON. An article's `name` is a string when you
  read it; its `body` is Tiptap JSON, rendered with [`<RichText>`](https://developers.uidu.org/docs/tools/react/components.md).
* **Writes** need the Bearer client. `createKbCollection` returns the new collection at
  `kbCollection.node`; `createKbArticle` creates the article in `attributes.kbCollectionId` and
  returns it at `kbArticle.node`. `update*` take `{ input: { id, attributes } }`, `delete*`
  `{ input: { id } }`. Validation failures come back in `errors`, not as a throw:
  [Writes](https://developers.uidu.org/docs/foundations/writes.md). Collection attributes: `name`, `description`, `iconData`,
  `position`. Article attributes: `name`, `body`, `isDraft`, `kbCollectionId`, `position`,
  `workspaceTagIds`.

For a ready-made help center (channel → collections → articles in one request), use
[`getChannel`](https://developers.uidu.org/docs/domains/help-center.md); reach for these functions when you don't want channel
scoping.

## Use cases

### Cross-channel article index

Goal: one page listing every published article in the workspace, grouped by collection.

```tsx
// src/app/kb/page.tsx
import Link from 'next/link';
import {
  ListKbArticlesDocument,
  listKbCollections,
  paginate,
} from '@uidu/client';
import { uidu } from '@/lib/uidu';

export default async function KbIndexPage() {
  const [collections, articles] = await Promise.all([
    listKbCollections(uidu, { first: 100 }),
    paginate(uidu, ListKbArticlesDocument, {
      variables: { first: 100 },
      extract: (data) => data.currentWorkspace?.kbArticles,
    }),
  ]);

  return collections
    .sort((a, b) => a.position - b.position)
    .map((c) => (
      <section key={c.id}>
        <h2>{typeof c.name === 'string' ? c.name : c.name?.en}</h2>
        <ul>
          {articles
            .filter((a) => a.kbCollection?.id === c.id)
            .sort((a, b) => a.position - b.position)
            .map((a) => (
              <li key={a.id}>
                <Link href={`/kb/article/${encodeURIComponent(a.id)}`}>{a.name}</Link>
              </li>
            ))}
        </ul>
      </section>
    ));
}
```

* `paginate` walks every page of articles; `listKbArticles(uidu, { first: 100 })` would stop at the
  first 100.
* The article page is `getKbArticle(uidu, { id })` + `<RichText doc={article.body} />` — the same
  route as the [Help Center article page](https://developers.uidu.org/docs/domains/help-center.md).

### Sync articles from your own docs

Goal: a server job that creates an article in a collection, then publishes it once its body is in.

```ts
// scripts/sync-kb.ts — runs with the apiKey client, never in a browser
import { createKbArticle, updateKbArticle } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

export async function upsertArticle(kbCollectionId: string, title: string, body: unknown) {
  const created = await createKbArticle(uiduAdmin, {
    input: { attributes: { kbCollectionId, name: title, body, isDraft: true } },
  });
  if (!created || created.errors?.length) {
    throw new Error(created?.errors?.map((e) => e.name).join(', ') ?? 'createKbArticle failed');
  }

  const id = created.kbArticle!.node!.id;
  const published = await updateKbArticle(uiduAdmin, {
    input: { id, attributes: { isDraft: false } },
  });
  if (published?.errors?.length) {
    throw new Error(published.errors.map((e) => e.name).join(', '));
  }
  return id;
}
```

* The new article is at `kbArticle.node` on create, but at `kbArticle` on update — the payloads
  differ.
* `body` is Tiptap JSON (`{ type: 'doc', content: [...] }`), the same shape `getKbArticle` returns.
* While `isDraft` is `true` the article stays out of `listKbArticles`, `getKbCollection` and the
  Help Center's `getChannel`.
* From the terminal: `uidu kb-articles create --attributes '{"kbCollectionId":"…","name":"…"}' --json`.
  Server client set-up: [Server integration](https://developers.uidu.org/docs/paths/server-integration.md).

## API at a glance

Every `@uidu/client` function in **Knowledge base**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#kb) has the rest of the SDK.

| Function                                                                                              | Kind  | Auth         | CLI                     | Description                                                                                                                                          |
| ----------------------------------------------------------------------------------------------------- | ----- | ------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createKbArticle`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer       | `kb-articles create`    | Create a knowledge-base article in `attributes.kbCollectionId`. Returns the payload (with `errors`) or null. The new article is at `kbArticle.node`. |
| [`createKbCollection`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `kb-collections create` | Create a knowledge-base collection. Returns the payload (with `errors`) or null. The new collection is at `kbCollection.node`.                       |
| [`deleteKbArticle`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer       | `kb-articles delete`    | Delete a knowledge-base article by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                 |
| [`deleteKbCollection`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `kb-collections delete` | Delete a knowledge-base collection by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                              |
| [`getKbArticle`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/kb.ts)              | read  | public token | `kb-articles get`       | One knowledge-base article with its rich-text `body` and its collection, or null.                                                                    |
| [`getKbCollection`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/kb.ts)           | read  | public token | `kb-collections get`    | One knowledge-base collection with its first 50 articles, or null.                                                                                   |
| [`listKbArticles`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/kb.ts)            | read  | public token | `kb-articles list`      | Knowledge-base articles across every collection, as a flat array; page with `first`/`after` (or `last`/`before`).                                    |
| [`listKbCollections`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/kb.ts)         | read  | public token | `kb-collections list`   | The workspace's knowledge-base collections as a flat array; page with `first`/`after` (or `last`/`before`).                                          |
| [`updateKbArticle`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer       | `kb-articles update`    | Update a knowledge-base article: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                       |
| [`updateKbCollection`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `kb-collections update` | Update a knowledge-base collection: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                    |

**Types:** `KbArticle`, `KbCollection`

**CLI:**

| Entity           | list | get | create | update | delete |
| ---------------- | :--: | :-: | :----: | :----: | :----: |
| `kb-collections` |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `kb-articles`    |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |

---

# Models (custom data)

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

> Define your own record types — Models with typed Fields — and store, read and update their ModelItems.

* **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)
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

A **Model** is a record type you define yourself: a name, a list of typed **Fields**, and the
**ModelItems** stored in it. It is uidu's data engine — the same one behind the new CMS — and
the place a [custom app](https://developers.uidu.org/docs/paths/custom-app.md) keeps the data that is its own: the bookings of a
room, a checklist, a log.

## Mental model

<Callout type="warn" title="Read what the workspace already has">
  A uidu workspace is not empty: it already holds goals, contacts, deals, people, events,
  courses… &#x2A;*Create a Model only for data uidu has no place for.** An app asking for "the OKRs
  behind schedule" or "our open deals" should read [Goals](https://developers.uidu.org/docs/domains/goals.md) or [Contacts](https://developers.uidu.org/docs/domains/contacts.md),
  not copy them into a Model — the copy drifts from the real records. When the data exists but a
  custom app can't reach it yet, say so rather than rebuilding it here.
</Callout>

```
WorkspaceApp  (the installed app — custom apps get one each)
  └─→ Model            (name, namePlural)            "Booking"
        ├─→ Field      (shortname, kind, required)   room: string, when: datetime
        │     └─→ FieldOption                         for singleSelect / multipleSelect
        └─→ ModelItem                                 one booking
              └─→ FieldValue  (field + content: { value })
```

* Every read goes through `node(id:)`, starting from a **WorkspaceApp id** — inside a custom
  app that is `context.workspaceApp.id` from the [app bridge](https://developers.uidu.org/docs/paths/custom-app.md). This is the only
  query root a custom app's session token may call. A read whose id isn't visible (or isn't
  the right type) returns `[]` or `null`, not an error.
* **Auth:** a custom app's session token (the member, framed in uidu) or an account Bearer
  token (`apiKey`, server-side only). A `publicToken` alone doesn't reach a WorkspaceApp's
  models. Writes are server-side with the `apiKey` — except a custom app writing its own Models
  with the member's session, in the browser.
* **Shapes.** A `Model` comes with its schema: `id`, `name`, `namePlural`, `fieldList[]` (`id`,
  `name`, `shortname`, `kind`, `required`, `position`, `optionList[]` of `{ id, name, color,
  position }`). A `ModelItem` has `id`, `name`, `createdAt`, `updatedAt` and
  `fieldValuesByShortname` — `{ room: 'Aula Magna', when: '…' }`, the easiest shape to read
  values from. `getModelItem` adds `model.id`, the model the item belongs to.

### Field kinds

Field `kind`s you will normally use: `string` (one line), `text` (several lines), `number`,
`currency`, `percent`, `date`, `datetime`, `checkbox`, `email`, `phone`, `url`, `rating`,
`member` (a person of the workspace). `singleSelect` / `multipleSelect` also need their options
(`createFieldOption`, see [Provision a Model from a script](#provision-a-model-from-a-script)) —
prefer `string` unless the choices matter. uidu knows more kinds (`attachments`, `linkedRecord`,
`formula`, `progress`, `richText`…); leave them out unless you need one.

A new field goes on a model with `fieldableId` (the model) **and** `fieldableType: 'Model'` — a
custom app may only name a type that matches the record it loads. A new model goes in a
WorkspaceApp with `modelableId`.

### `ensureModel` only adds

`ensureModel` finds the app's model **by name**, or creates it — together with any of `fields`
it is missing, **matched by shortname** — and returns it with all its fields. Fields the model
already has are left alone, extra fields are never removed, and a field whose `kind` changed is
not altered. It is what a custom app runs on its first load, until model schemas are created
from the app's manifest at install time. When a write is refused it throws a `UiduError` with
code `MUTATION_ERROR` (unlike the other writes, which return `errors`).

<Callout type="warn" title="Call it once per app instance">
  Two concurrent `ensureModel` calls each see "no model yet" and each create one. Memoize the
  promise (load models once, at the top of the app) instead of calling it from every component.
</Callout>

### Values are `content: { value }`

A write takes `fieldValuesAttributes`: one `{ fieldId, content: { value } }` per field.
`toFieldValuesAttributes(model, { room: 'Aula Magna' })` builds it from a `{ shortname: value }`
object: it resolves each shortname to its field id, wraps the value, skips `undefined` values,
and throws a `UiduError` with code `UNKNOWN_FIELD` for a shortname the model doesn't have — so
pass a model loaded with its `fieldList` (what `ensureModel`, `listModels` and `getModel`
return).

<Callout type="info" title="content is always { value: … }">
  The API stores each field value in a JSON `content` column and reads it back as
  `content.value`, for **every** kind: `{ value: 42 }`, `{ value: true }`,
  `{ value: '2026-07-27' }`, `{ value: ['gid://…'] }`. A bare value is accepted by the server but
  silently lost. `toFieldValuesAttributes` builds the right shape, and every `@uidu/client`
  mutation also wraps a bare `content` for you (with a dev-time warning) — but write it wrapped
  anyway, other clients of the API won't fix it up. See [Forms](https://developers.uidu.org/docs/domains/forms.md).
</Callout>

### Writes return the payload

Writes return the mutation payload as-is rather than throwing — always check `errors`. The
record's key follows the entity, with no `node` wrapper:

| Write                                                         | Payload                                               |
| ------------------------------------------------------------- | ----------------------------------------------------- |
| `createModel`, `updateModel`                                  | `{ errors, model }` — with its `fieldList`            |
| `createField`, `updateField`, `deleteField`                   | `{ errors, field }`                                   |
| `createFieldOption`, `updateFieldOption`, `deleteFieldOption` | `{ errors, fieldOption }`                             |
| `createModelItem`, `updateModelItem`                          | `{ errors, modelItem }` — same shape as a listed item |
| `deleteModelItem`                                             | `{ errors, modelItem: { id } }`                       |
| `updateFieldValue` / `deleteFieldValue`                       | `{ errors, fieldValue }`                              |

Each one resolves to `null` when the server returns no payload.

### Two behaviours that bite

* **`listModelItems` lags behind writes.** It reads uidu's search index, which catches up a
  moment after a write. After `createModelItem` / `deleteModelItem`, update your list from the
  mutation payload instead of re-listing straight away, or the new item will be missing. Its
  optional `params` are search parameters, the same shape as [`search`](https://developers.uidu.org/docs/domains/search.md).
* **`updateModelItem` appends, it doesn't edit in place.** Entries in `fieldValuesAttributes`
  without an `id` are written as new field values; `fieldValuesByShortname` reports the most
  recently written one per field. See [Change a record](#change-a-record).

## Use cases

### A custom app's own records

Goal: a custom app framed in uidu keeps its bookings in a Model of its own WorkspaceApp, with the
member's session — no token to configure.

```tsx
'use client';
import { useEffect, useState } from 'react';
import { useUiduApp } from '@uidu/react';
import {
  createModelItem,
  deleteModelItem,
  ensureModel,
  listModelItems,
  toFieldValuesAttributes,
  type Model,
  type ModelItem,
  type UiduClient,
} from '@uidu/client';

// One promise per app instance: concurrent ensureModel calls would each create the model.
let bookingModel: Promise<Model> | null = null;
function loadBookingModel(client: UiduClient, workspaceAppId: string) {
  bookingModel ??= ensureModel(client, {
    workspaceAppId,
    name: 'Booking', // matched against existing models by name
    namePlural: 'Bookings',
    fields: [
      // matched by shortname; missing ones are created
      { shortname: 'room', name: 'Room', kind: 'string', required: true },
      { shortname: 'when', name: 'When', kind: 'datetime' },
    ],
  });
  return bookingModel;
}

export function useBookings() {
  const app = useUiduApp();
  const client = app.status === 'ready' ? app.client : null;
  const workspaceAppId = app.status === 'ready' ? app.context.workspaceApp.id : null;
  const [model, setModel] = useState<Model | null>(null);
  const [items, setItems] = useState<ModelItem[]>([]);

  useEffect(() => {
    if (!client || !workspaceAppId) return;
    loadBookingModel(client, workspaceAppId).then(async (m) => {
      setModel(m);
      setItems(await listModelItems(client, { modelId: m.id }));
    });
  }, [client, workspaceAppId]);

  async function add(room: string, when: Date) {
    if (!client || !model) return;
    const result = await createModelItem(client, {
      input: {
        attributes: {
          modelId: model.id,
          fieldValuesAttributes: toFieldValuesAttributes(model, {
            room,
            when: when.toISOString(),
          }),
        },
      },
    });
    if (result?.errors?.length) throw new Error(result.errors[0].name);
    setItems((prev) => [...prev, result!.modelItem!]); // don't re-list: the index lags
  }

  async function remove(id: string) {
    if (!client) return;
    const result = await deleteModelItem(client, { input: { id } });
    if (!result?.errors?.length) setItems((prev) => prev.filter((i) => i.id !== id));
  }

  return { items, add, remove };
}
```

* Read a value with `(item.fieldValuesByShortname ?? {}) as Record<string, unknown>` —
  `values.room` is `'Aula Magna'`, `values.when` an ISO timestamp.
* The session exists only in the browser, after the bridge connects; the whole flow, and what to
  show when the app is opened outside uidu: [Custom app](https://developers.uidu.org/docs/paths/custom-app.md). A working
  version is the [demo's custom app](https://github.com/uidu-org/api.js/tree/main/apps/demo/src/app/custom-app).

### Provision a Model from a script

Goal: set up a Model's schema — including a select field and its options — and seed it from a
server-side script, ahead of the app's first load.

```ts
// scripts/provision-bookings.ts — runs on your machine or server, never in a browser
import {
  createClient,
  createFieldOption,
  createModelItem,
  ensureModel,
  listModels,
  toFieldValuesAttributes,
} from '@uidu/client';

const uiduAdmin = createClient({
  workspace: process.env.UIDU_WORKSPACE!,
  apiKey: process.env.UIDU_API_KEY,
});
const workspaceAppId = process.argv[2]; // the installed app's WorkspaceApp id

const existing = await listModels(uiduAdmin, { workspaceAppId });
console.log(existing.map((m) => `${m.name}: ${m.fieldList?.map((f) => f.shortname).join(', ')}`));

const model = await ensureModel(uiduAdmin, {
  workspaceAppId,
  name: 'Booking',
  namePlural: 'Bookings',
  fields: [
    { shortname: 'room', name: 'Room', kind: 'string', required: true },
    { shortname: 'when', name: 'When', kind: 'datetime' },
    { shortname: 'status', name: 'Status', kind: 'singleSelect' },
  ],
});

const status = model.fieldList?.find((f) => f.shortname === 'status');
if (status && !status.optionList?.length) {
  for (const [name, color] of [['Requested', 'yellow'], ['Confirmed', 'green']]) {
    const result = await createFieldOption(uiduAdmin, {
      input: { attributes: { fieldId: status.id, name, color } },
    });
    if (result?.errors?.length) throw new Error(result.errors[0].name);
  }
}

const seeded = await createModelItem(uiduAdmin, {
  input: {
    attributes: {
      modelId: model.id,
      fieldValuesAttributes: toFieldValuesAttributes(model, {
        room: 'Aula Magna',
        when: new Date('2026-10-12T09:00').toISOString(),
      }),
    },
  },
});
if (seeded?.errors?.length) throw new Error(seeded.errors[0].name);
```

* `ensureModel` makes the script safe to re-run: it only adds what's missing.
* Below it are the lower-level writes it uses: `createModel(client, { input: { attributes: {
  modelableId, name, namePlural } } })` and `createField(client, { input: { attributes: {
  fieldableId, fieldableType: 'Model', name, shortname, kind } } })`. `updateModel`,
  `updateField` and `deleteField` take `{ input: { id, attributes } }` / `{ input: { id } }`, and
  so do `updateFieldOption` / `deleteFieldOption`.
* `listModels` returns `[]` when the WorkspaceApp isn't visible to the token; `getModel(client, { id })` reads one model by id.

### Change a record

Goal: move a booking to another room, and know what the item reports afterwards.

```ts
import { updateModelItem, toFieldValuesAttributes } from '@uidu/client';

const result = await updateModelItem(client, {
  input: {
    id: item.id,
    attributes: {
      fieldValuesAttributes: toFieldValuesAttributes(model, { room: 'Sala riunioni 1' }),
    },
  },
});

if (!result?.errors?.length) {
  result?.modelItem?.fieldValuesByShortname; // { room: 'Sala riunioni 1', when: … }
}
```

* The `room` entry has no `id`, so it is written as a **new** field value next to the old one;
  `fieldValuesByShortname` reports the most recent. That's what you read back, but the old
  value is still stored.
* To change an existing field value in place, use `updateFieldValue(client, { input: { id,
  attributes: { content: { value } } } })` with that value's id; `deleteFieldValue(client, {
  input: { id } })` removes one, and `deleteLinkedRecord: true` also deletes the record it
  links to. The `ModelItem` the SDK reads carries values by shortname, not their ids.
* `getModelItem(client, { id })` re-reads one item, with the id of its model.

## API at a glance

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

| Function                                                                                                | Kind   | Auth   | CLI | Description                                                                                                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------- | ------ | ------ | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createField`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Pass `fieldableId` (the model) with `fieldableType: 'Model'`: a custom app may only name a type that matches the record it loads.                                                                                                                                                                        |
| [`createFieldOption`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)       | write  | Bearer | —   | Add an option to a select-kind field (`attributes.fieldId`, `name`, `color`). Returns the payload (with `errors`) or null.                                                                                                                                                                               |
| [`createModel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Create a model in a WorkspaceApp (`attributes.modelableId`, `name`, `namePlural`). Returns the payload (with `errors`) or null.                                                                                                                                                                          |
| [`createModelItem`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)         | write  | Bearer | —   | Pass `attributes.modelId` and `attributes.fieldValuesAttributes` — build the latter by shortname with `toFieldValuesAttributes`.                                                                                                                                                                         |
| [`deleteField`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Delete a field from its model by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                       |
| [`deleteFieldOption`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)       | write  | Bearer | —   | Delete a select option by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                              |
| [`deleteFieldValue`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)        | write  | Bearer | —   | Clear a single value by its FieldValue id; `deleteLinkedRecord: true` also deletes the record it links to. Returns the payload (with `errors`) or null.                                                                                                                                                  |
| [`deleteModelItem`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)         | write  | Bearer | —   | Delete an item by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                      |
| [`ensureModel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Find the app's model by name, or create it — and any of `fields` it lacks. Returns the model with all its fields. What a custom app runs on its first load until schemas come from an install-time manifest (custom apps spec, step 3). Throws a `UiduError` (`MUTATION_ERROR`) when a write is refused. |
| [`getModel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)                | read   | Bearer | —   | One model with its fields, or null when the id is not a Model.                                                                                                                                                                                                                                           |
| [`getModelItem`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)            | read   | Bearer | —   | One model item with its values and its `model.id`, or null when the id is not a ModelItem.                                                                                                                                                                                                               |
| [`listModelItems`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)          | read   | Bearer | —   | A model's items. Each carries `fieldValuesByShortname` — `{ room: 'Aula Magna', when: '…' }` — the easiest shape to read values from.                                                                                                                                                                    |
| [`listModels`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)              | read   | Bearer | —   | The models of a WorkspaceApp, with their fields.                                                                                                                                                                                                                                                         |
| [`toFieldValuesAttributes`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts) | helper | —      | —   | `{ room: 'Aula Magna' }` → `[{ fieldId: '<id of room>', content: { value: 'Aula Magna' } }]`, for `createModelItem` / `updateModelItem`. Throws on a shortname the model doesn't have; skips `undefined` values.                                                                                         |
| [`updateField`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Update a field (name, `required`, preferences…): `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                           |
| [`updateFieldOption`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)       | write  | Bearer | —   | Update a select option: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                    |
| [`updateFieldValue`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)        | write  | Bearer | —   | Set a single value by its FieldValue id: `{ input: { id, attributes: { content: { value } } } }`. Returns the payload (with `errors`) or null.                                                                                                                                                           |
| [`updateModel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Rename or reconfigure a model: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                             |
| [`updateModelItem`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)         | write  | Bearer | —   | Update an item: `{ input: { id, attributes: { fieldValuesAttributes } } }`, built with `toFieldValuesAttributes`. Returns the payload (with `errors`) or null.                                                                                                                                           |

**Types:** `EnsureModelOptions`, `Model`, `ModelField`, `ModelFieldOption`, `ModelItem`

---

# People

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

> Employees, contracts, pay and benefits, shifts and time clocks — private HR data for server-side tools and scripts.

* **Use it for:** [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** Bearer (`apiKey`) — server-side only
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu employees`, `uidu employments`, `uidu offices`, `uidu roles`, `uidu ccnls`, `uidu time-clocks`, `uidu compensations`, `uidu benefits`, `uidu benefit-enrollments`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

People is uidu's HR module: employees, their contracts (employments) with pay and benefits,
scheduled shifts and time-clock punches. Use it for internal dashboards, payroll exports and
scripts that hire, change and end contracts. Everything here is private data, so every call —
read or write — runs server-side with an `apiKey` client.

## Mental model

```
Employee            HR record of a workspace user (userId) — employeeCode, kioskPin
  ├─→ Employment    a contract: startDate / endDate / endReason, jobTitle, employmentKind,
  │     │           ccnl, contractLevel, fteRatio, roleId, circleId, offices, working hours
  │     ├─→ Compensation       pay item: amount, currency, frequency, componentType, effectiveFrom
  │     └─→ BenefitEnrollment  the contract enrolled in a Benefit (workspace catalog)
  ├─→ Shifts        scheduled work — beginsAt / finishesAt / aasmState
  ├─→ TimeClocks    actual punches — beginsAt / finishesAt (null while still clocked in)
  └─→ TimeEntries   attributed hours, anchored to a Shift + TimeClock (schema only, not in the SDK)

Reference data an employment points at:
  Office (listOffices) · role Circle (listRoles) · org-unit Circle (listCircles) · CCNL (listCcnls)
```

* **The name is on the user.** An employee has no top-level `name`: read `employee.user?.name`
  (also `firstName`, `lastName`, `email`). Likewise the contract type is
  `currentEmployment.employmentKind.name` (e.g. `Tempo indeterminato`) — there is no
  `contractType` field on the API. It used to be a free string; uidu promoted it to an
  `EmploymentKind` record so each workspace configures its own, which is why writes take
  `employmentKindId`. Only the records `listEmploymentHistory` builds carry a flat
  `contractType` (a copy of `employmentKind.name`) for display.
* **Contract events are employment writes.** A hire (*assunzione*) is `createEmployment`; a
  variation (*variazione*) is a new `createEmployment`, or `updateEmployment` on the current one;
  a termination (*cessazione*) is `updateEmployment` with `endDate` + `endReason` — which is
  exactly what `terminateEmployment` sends. `deleteEmployment` erases the record; to end a contract,
  terminate it. `endReason` is one of `resignation`, `termination`,
  `contract_end`, `retirement`, `other`, or the finer-grained
  `resignation_just_cause`, `dismissal_just_cause`, `dismissal_subjective_reason`,
  `dismissal_objective_reason`, `collective_dismissal`, `mutual_agreement`, `probation_failed`,
  `death` and `business_closure`.
* **Units.** `fteRatio` is a fraction (`1.0` full-time, `0.5` half-time). Dates (`startDate`,
  `endDate`, `effectiveFrom`, `enrolledAt`, `weekStartingAt`) are `ISO8601Date` strings
  (`YYYY-MM-DD`); punches (`beginsAt`, `finishesAt`) are `ISO8601DateTime`. Compensation and
  benefit amounts are plain `Float`s in the given `currency` — not cents.
* **Schedules live on the employee, not the workspace.** `getEmployee` returns `todayTimeClocks`
  and `weeklyShifts` (first 20) for the week starting `weekStartingAt` — a required Monday date
  that `weekStartFor(date?)` computes in UTC: `weekStartFor(new Date('2026-05-15'))` →
  `'2026-05-11'`, `weekStartFor(new Date('2026-01-01'))` → `'2025-12-29'`; with no argument, the
  current week. A shift's `aasmState` is e.g. `planned`, `confirmed`, `completed`.
* **Reference data.** `listOffices` gives the ids for `employmentOfficesAttributes`
  (`[{ officeId, isPrimary }]`). `listCircles` returns two flavours of Circle told apart by
  `kind`: org units (an employment's `circleId`) and roles (`kind === 'role'`, an employment's
  `roleId`) — `listRoles` returns only the roles. `listKinds(client, { models })` reads the
  polymorphic `Kind` records; `listCcnls` is `listKinds` with `models: ['Ccnl']`. There is no
  list function for employment kinds: take an `employmentKind.id` from an existing employment.
* **Two kinds of CCNL record.** `listCcnls` returns `Kind`-tagged rows (`id`, `name`, `code`,
  `model`). `createCcnl` creates the first-class `Ccnl` record (code, sector, contract levels via
  `ccnlLevelsAttributes`), and that is the type `Employment.ccnl` / `ccnlId` point at. When you
  create a CCNL, use the id it returns (`ccnl.node.id`) as `ccnlId`.
* **Payload shapes.** Writes return `{ errors, … }` and never throw on validation. Creates wrap the
  record in an edge — `employee.node`, `employment.node`, `compensation.node`, `timeClock.node`,
  `ccnl.node`, `benefit.node`, `benefitEnrollment.node` — while updates return it directly
  (`employment`, `employee`, `compensation`).
* **Not in the SDK yet.** There is no workspace-wide list of shifts or time entries — iterate
  employees and read `weeklyShifts` per employee. Time entries, approving time off, and the
  `weeklyTimeOffs` / `monthlyTimeOffs` fields exist in the schema but aren't wrapped; reach them
  through [`executeAction`](https://developers.uidu.org/docs/paths/server-integration.md) where an action exists.

## Use cases

### HR dashboard of active contracts

Goal: a server-rendered table of everyone with an active contract, and a detail panel with an
employee's punches today and shifts this week.

```ts
// app/hr/data.ts — server-only
import {
  paginate,
  ListEmployeesDocument,
  getEmployee,
  weekStartFor,
} from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin'; // createClient({ workspace, apiKey })

export async function activeContracts() {
  const employees = await paginate(uiduAdmin, ListEmployeesDocument, {
    variables: { first: 100 },
    extract: (data) => data.currentWorkspace?.employees,
  });

  return employees
    .filter((e) => e.currentEmployment?.isActive)
    .map((e) => ({
      id: e.id,
      name: e.user?.name ?? e.user?.email, // the name is on the user
      code: e.employeeCode,
      jobTitle: e.currentEmployment?.jobTitle,
      contract: e.currentEmployment?.employmentKind?.name, // 'Tempo indeterminato'
      since: e.currentEmployment?.startDate,
    }));
}

export async function employeeWeek(id: string) {
  const employee = await getEmployee(uiduAdmin, {
    id,
    weekStartingAt: weekStartFor(), // Monday of this week, 'YYYY-MM-DD'
  });
  if (!employee) return null;

  return {
    name: employee.user?.name,
    fte: employee.currentEmployment?.fteRatio, // 1.0 = full-time
    level: employee.currentEmployment?.contractLevel,
    office: employee.currentEmployment?.primaryOffice?.name,
    clockedIn: employee.todayTimeClocks.some((tc) => !tc.finishesAt),
    shifts: (employee.weeklyShifts?.edges ?? []).map((edge) => ({
      begins: edge?.node?.beginsAt,
      ends: edge?.node?.finishesAt,
      state: edge?.node?.aasmState, // 'planned' | 'confirmed' | 'completed' | …
    })),
  };
}
```

* `listEmployees(uiduAdmin, { first: 50 })` returns one page as a flat array; `paginate` with
  `ListEmployeesDocument` walks them all — see
  [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).
* The list carries only a summary of the current employment (`jobTitle`, `employmentKind`,
  `isActive`, `startDate`); `contractLevel`, `classification`, `fteRatio`, `endDate` and
  `primaryOffice` come from `getEmployee`.

### Hire, change and terminate from a script

Goal: a payroll sync or an onboarding script that opens a contract for a new hire, adds the
salary and a benefit, and later closes the contract.

```ts
// scripts/hire.ts — Node, server-side
import {
  createEmployee,
  createEmployment,
  createCompensation,
  createBenefitEnrollment,
  terminateEmployment,
  listOffices,
  listRoles,
  listEmployees,
} from '@uidu/client';
import { uiduAdmin } from './uidu-admin';

declare const userId: string; // an existing workspace member
declare const benefitId: string; // from createBenefit, or your catalog

// 1. Reference data for the contract form
const [offices, roles, employees] = await Promise.all([
  listOffices(uiduAdmin),
  listRoles(uiduAdmin), // role-Circles → roleId
  listEmployees(uiduAdmin, { first: 50 }),
]);
// no list function for employment kinds: reuse one from an existing contract
const permanent = employees.find(
  (e) => e.currentEmployment?.employmentKind?.name === 'Tempo indeterminato',
)?.currentEmployment?.employmentKind?.id;

// 2. The HR record for an existing workspace user
const created = await createEmployee(uiduAdmin, {
  input: { attributes: { userId, employeeCode: 'E-0042' } },
});
if (created?.errors?.length) throw new Error(created.errors.map((e) => e.name).join(', '));
const employeeId = created!.employee!.node!.id; // creates wrap the record in `.node`

// 3. Hire (assunzione) = createEmployment
const hired = await createEmployment(uiduAdmin, {
  input: {
    attributes: {
      employeeId,
      startDate: '2026-11-01',
      jobTitle: 'Educatore',
      employmentKindId: permanent,
      roleId: roles[0]?.id,
      fteRatio: 0.5, // half-time
      employmentOfficesAttributes: [{ officeId: offices[0]!.id, isPrimary: true }],
    },
  },
});
const employmentId = hired!.employment!.node!.id;

// 4. Pay and benefits hang off the employment, not the employee
await createCompensation(uiduAdmin, {
  input: {
    attributes: {
      employmentId,
      amount: 1850, // a plain amount in `currency`, not cents
      currency: 'EUR',
      frequency: 'monthly',
      componentType: 'base',
      effectiveFrom: '2026-11-01',
    },
  },
});
await createBenefitEnrollment(uiduAdmin, {
  input: { attributes: { benefitId, employmentId, enrolledAt: '2026-11-01' } },
});

// 5. Later: termination (cessazione) closes the contract — it isn't deleted
const ended = await terminateEmployment(uiduAdmin, {
  id: employmentId,
  endDate: '2027-10-31',
  endReason: 'contract_end',
});
ended?.employment?.endDate; // updates return the record directly, no `.node`
```

* A **variation** (new level, new hours, new role) is either a new `createEmployment` from the
  change date — which `contractEvents` will report as a `variation` — or `updateEmployment`
  (`{ input: { id, attributes } }`) on the current contract when you don't need the history.
* `createBenefit` adds to the workspace's benefit catalog (`name`, `category`, `provider`,
  `employerCostAmount`, `employeeCostAmount`, …); its id is at `benefit.node.id`.
* Punches can be written too: `createTimeClock(uiduAdmin, { input: { attributes: { resourceId,
  beginsAt, finishesAt } } })`, the new punch at `timeClock.node`.
* The same writes from a terminal: `uidu employments create --attributes '{"employeeId",…}'`,
  `uidu employments update <id> --attributes '{"endDate","endReason"}'` — see
  [CLI commands](https://developers.uidu.org/docs/tools/cli/commands.md).

### Monthly report of hires, variations and terminations

Goal: the Italian monthly HR summary — *assunzioni*, *variazioni*, *cessazioni* — from the full
contract history.

```ts
import { listEmploymentHistory, contractEvents } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

const history = await listEmploymentHistory(uiduAdmin, { first: 100 });
const events = contractEvents(history); // most recent first

const byMonth = events.reduce<Record<string, typeof events>>((acc, e) => {
  (acc[e.month] ??= []).push(e); // 'YYYY-MM'
  return acc;
}, {});

for (const [month, list] of Object.entries(byMonth)) {
  const count = (t: string) => list.filter((e) => e.type === t).length;
  console.log(month, {
    hires: count('hire'),
    variations: count('variation'),
    terminations: count('termination'),
  });
}

events
  .filter((e) => e.type === 'termination')
  .map((e) => `${e.date} ${e.employeeName} — ${e.contractType} (${e.endReason})`);
```

* `listEmploymentHistory` returns every employee with **all** their contracts (up to
  `employmentsFirst`, default 50), not just the current one. Each record is flattened:
  `{ id, name, employments: [{ id, startDate, endDate, endReason, jobTitle, employmentKind,
  contractType, ccnl, compensations }] }`, with `name` falling back to the user's email, then
  `'Senza nome'`. Compensations (first 20) come most recent `effectiveFrom` first.
* `contractEvents` rules: per employee, contracts are sorted by `startDate`; the earliest is a
  `hire`, every later one a `variation`, both on their start date; any contract with an `endDate`
  also yields a `termination` on it, carrying `endReason`. Contracts without a `startDate` are
  ignored. Each event has `date` (`YYYY-MM-DD`) and `month` (`YYYY-MM`) for grouping.
* `first`/`after` page the employees, but the flattened result carries no cursor: pass a
  `first` that covers the whole workforce.

## API at a glance

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

| Function                                                                                                             | Kind   | Auth   | CLI                          | Description                                                                                                                                                                                                                                                                                                                                                                                                             |
| -------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`contractEvents`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                       | helper | —      | —                            | Flatten employment history into dated contract events, most recent first: - the employee's earliest contract is a `hire` (assunzione) on its start date; - every later contract is a `variation` (variazione) on its start date; - any contract with an end date yields a `termination` (cessazione) on it.                                                                                                             |
| [`createBenefit`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/benefits.ts)                      | write  | Bearer | `benefits create`            | Create a benefit in the workspace's catalog (name, category, provider, employer/employee costs). Returns the payload (with `errors`) or null; it is at `benefit.node`.                                                                                                                                                                                                                                                  |
| [`createBenefitEnrollment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/benefit-enrollments.ts) | write  | Bearer | `benefit-enrollments create` | Enroll an employment in a benefit (`benefitId`, `employmentId`, `enrolledAt`). Returns the payload (with `errors`) or null; it is at `benefitEnrollment.node`.                                                                                                                                                                                                                                                          |
| [`createCcnl`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                           | write  | Bearer | `ccnls create`               | Create a CCNL (national collective labor contract) — the first-class `Ccnl` record (code, sector, contract levels), distinct from the `Kind`-tagged rows `listCcnls` returns above. `Employment.ccnl` / `ccnlId` point at this first-class type. Requires a Bearer token (`apiKey`).                                                                                                                                    |
| [`createCompensation`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                | write  | Bearer | `compensations create`       | Add a pay item to an employment (`attributes.employmentId`, `amount`, `currency`, `frequency`, `effectiveFrom`). Returns the payload (with `errors`) or null. The new item is at `compensation.node`.                                                                                                                                                                                                                   |
| [`createEmployee`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                    | write  | Bearer | `employees create`           | Create the HR record for a workspace user (`attributes.userId`). Returns the payload (with `errors`) or null. The new employee is at `employee.node`.                                                                                                                                                                                                                                                                   |
| [`createEmployment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                  | write  | Bearer | `employments create`         | Hire: open a new employment (contract) for `attributes.employeeId` with its start date, kind, role and hours. Returns the payload (with `errors`) or null. The new employment is at `employment.node`.                                                                                                                                                                                                                  |
| [`createTimeClock`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                   | write  | Bearer | `time-clocks create`         | Record a time-clock punch (`beginsAt`, optional `finishesAt`, `resourceId`). Returns the payload (with `errors`) or null. The new punch is at `timeClock.node`.                                                                                                                                                                                                                                                         |
| [`deleteCompensation`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                | write  | Bearer | `compensations delete`       | Delete a pay item by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                                                                                  |
| [`deleteEmployment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                  | write  | Bearer | `employments delete`         | Delete an employment by `{ input: { id } }` (to end a contract, use `terminateEmployment` instead). Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                        |
| [`getEmployee`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                          | read   | Bearer | —                            | One employee with current employment, today's time clocks and the shifts of the week starting `weekStartingAt` (see `weekStartFor`), or null.                                                                                                                                                                                                                                                                           |
| [`listCcnls`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                            | read   | Bearer | `ccnls list`                 | List the CCNLs (national collective labor contracts) configured for the workspace — used for an employment's `ccnlId`.                                                                                                                                                                                                                                                                                                  |
| [`listCircles`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                          | read   | Bearer | —                            | List the workspace circles. Circles come in two flavours distinguished by `kind`: org units (e.g. departments — used as an employment's `circleId`) and roles (`kind === 'role'` — used as an employment's `roleId`). Use `listRoles` when you only want the role-Circles.                                                                                                                                              |
| [`listEmployees`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                        | read   | Bearer | `employees list`             | The workspace's employees with their current employment, as a flat array; page with `first`/`after`.                                                                                                                                                                                                                                                                                                                    |
| [`listEmploymentHistory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                | read   | Bearer | —                            | List every employee with their full employment (contract) history — not just the current one. Use `contractEvents` to turn this into a flat, dated list of hires, terminations and contract variations.                                                                                                                                                                                                                 |
| [`listKinds`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                            | read   | Bearer | —                            | List `Kind` records for the given polymorphic models.                                                                                                                                                                                                                                                                                                                                                                   |
| [`listOffices`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                          | read   | Bearer | `offices list`               | List the workspace offices — used for an employment's `employmentOfficesAttributes`.                                                                                                                                                                                                                                                                                                                                    |
| [`listRoles`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                            | read   | Bearer | `roles list`                 | List only the role-Circles (structured job titles) — an employment's `roleId`.                                                                                                                                                                                                                                                                                                                                          |
| [`terminateEmployment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)               | write  | Bearer | —                            | Terminate (cessazione) an employment by closing it with an end date and reason. Thin semantic wrapper over `updateEmployment`. `endReason` is one of: resignation, termination, contract\_end, retirement, other, resignation\_just\_cause, dismissal\_just\_cause, dismissal\_subjective\_reason, dismissal\_objective\_reason, collective\_dismissal, mutual\_agreement, probation\_failed, death, business\_closure. |
| [`updateCompensation`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                | write  | Bearer | `compensations update`       | Update a pay item: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                                                                        |
| [`updateEmployee`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                    | write  | Bearer | `employees update`           | Update an employee's HR record (code, kiosk PIN, time-clock flags): `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                       |
| [`updateEmployment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                  | write  | Bearer | `employments update`         | Update an employment: `{ input: { id, attributes } }`; to end one, prefer `terminateEmployment`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                           |
| [`weekStartFor`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                         | helper | —      | —                            | Compute the Monday of the week containing `date` as an ISO8601 date string. Useful for the `weekStartingAt` argument required by `getEmployee` (and any other weekly-aggregated employee fields).                                                                                                                                                                                                                       |

**Types:** `Circle`, `CompensationRecord`, `ContractEvent`, `ContractEventType`, `Employee`, `EmployeeContracts`, `EmploymentRecord`, `Kind`, `Office`

**CLI:**

| Entity                | list | get | create | update | delete |
| --------------------- | :--: | :-: | :----: | :----: | :----: |
| `employees`           |   ✓  |  —  |    ✓   |    ✓   |    —   |
| `employments`         |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `offices`             |   ✓  |  —  |    —   |    —   |    —   |
| `roles`               |   ✓  |  —  |    —   |    —   |    —   |
| `ccnls`               |   ✓  |  —  |    ✓   |    —   |    —   |
| `time-clocks`         |   —  |  —  |    ✓   |    —   |    —   |
| `compensations`       |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `benefits`            |   —  |  —  |    ✓   |    —   |    —   |
| `benefit-enrollments` |   —  |  —  |    ✓   |    —   |    —   |

---

# Search

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

> Run filtered, paginated search across every searchable record type in your workspace.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** none — read-only today
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

`search` queries the workspace's search index: one call, filtered and sorted, across the record
types you name. The index covers about forty of them — `Contact`, `Deal`, `Donation`,
`DonationCampaign`, `Event`, `Form`, `Product`, `Story`, `Task`, `Note`, `Employee`, `ModelItem` and
more (the `node` union of `SearchResult` lists them all).

## Mental model

```
search(client, { models, params, first, after })
└── SearchResult                 a Relay connection, not a flat array
    ├── totalCount, totalPages   how many matches
    ├── term                     the term the engine searched for
    ├── took                     query time, in ms
    ├── pageInfo                 { hasNextPage, endCursor }
    └── edges[]                  { cursor, node: { __typename, id } }
```

* **Hits are references.** Each `node` carries only `__typename` and `id`. Narrow on `__typename`,
  then fetch what you want to show with that domain's `get*` (`getEvent`, `getStory`, `getForm`…).
  The SDK's query is fixed, so you can't ask `search` for more fields per type.
* **`models`** restricts the search to record types: `['Event', 'Story']`. Always pass it. With a
  single model the engine applies that model's own defaults (scoping, newest first); a single model
  that isn't searchable fails the request.
* **Not indexed:** knowledge-base articles. To search a help center, filter its articles in memory
  ([Help Center](https://developers.uidu.org/docs/domains/help-center.md)).
* **Scope.** Results are limited to the client's workspace and leave out trashed (discarded)
  records. Your filters can narrow that scope, never widen it.
* **If the index fails** (missing, being rebuilt), you get an empty result rather than an error — so
  an empty result isn't proof that nothing matches.

**`params`** — every key is optional:

```ts
{
  term?: string;                       // full-text term; omitted = everything ('*')
  filters?: Array<{
    id: string;                        // the field, camelCase: 'createdAt', 'name'
    operator: string;                  // see below
    value?: unknown;                   // JSON
    kind?: string;                     // field kind: 'date', 'number', 'multipleSelect'…
  }>;
  sorting?: Array<{ id: string; desc?: boolean }>;
  grouping?: string[];
  options?: { /* engine options: fields, highlight, aggs, boostBy, misspellings… (JSON) */ };
}
```

| `operator`                       | Matches                                                                                      |
| -------------------------------- | -------------------------------------------------------------------------------------------- |
| `=` / `eq`                       | exactly `value` — case-sensitive; with `kind: 'multipleSelect'`, all of `value`              |
| `in`                             | any of `value` (an array)                                                                    |
| `!=` / `notIn`, `doesNotContain` | anything but `value`                                                                         |
| `contains`                       | `value` as a full-text term on that field — case-insensitive, fuzzy                          |
| `>`, `>=`, `<`, `<=`             | ranges on numbers and dates                                                                  |
| `between`, `notBetween`          | `value: [from, to]`; leave either end empty for an open range                                |
| `dateOverlap`                    | a date range that overlaps `value`                                                           |
| `null`, `notNull`                | empty or set; `kind: 'number'` or `'date'` treats only `null` as empty, other kinds `''` too |

Sorting: relevance comes first, then your `sorting` in order, then the default order (newest
first). Pagination is cursor-only: pass the last `pageInfo.endCursor` as `after`; there is
no going back. `hasNextPage` is `true` whenever a page comes back full, so when the total is an exact
multiple of `first` the last request returns an empty page.

## Use cases

### A search box over events and stories

Goal: `/search?q=…` shows matching events and stories with their titles, from one route.

```tsx
// src/app/search/page.tsx
import Link from 'next/link';
import { getEvent, getStory, search } from '@uidu/client';
import { uidu } from '@/lib/uidu';

export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q = '' } = await searchParams;
  if (q.trim().length < 2) return <p>Type at least two characters.</p>;

  const result = await search(uidu, {
    models: ['Event', 'Story'],
    params: { term: q },
    first: 10,
  });
  const hits = (result?.edges ?? []).flatMap((e) => (e?.node ? [e.node] : []));

  // Hits are { __typename, id }: fetch each one's title from its own domain.
  const rows = await Promise.all(
    hits.map(async (hit) => {
      switch (hit.__typename) {
        case 'Event': {
          const event = await getEvent(uidu, { id: hit.id });
          const href = `/event/${encodeURIComponent(hit.id)}`;
          return event && { id: hit.id, kind: 'Event', title: event.name, href };
        }
        case 'Story': {
          const story = await getStory(uidu, { id: hit.id });
          const href = `/stories/${encodeURIComponent(hit.id)}`;
          return story && { id: hit.id, kind: 'Story', title: story.name, href };
        }
        default:
          return null;
      }
    }),
  );

  return (
    <>
      <p>
        {result?.totalCount ?? 0} results in {result?.took ?? 0} ms
      </p>
      <ul>
        {rows.map(
          (r) =>
            r && (
              <li key={r.id}>
                <Link href={r.href}>{r.title}</Link> <small>· {r.kind}</small>
              </li>
            ),
        )}
      </ul>
    </>
  );
}
```

* Reads work with the public token, so the same call can run in the browser. Doing it in a Server
  Component keeps the follow-up `get*` calls in one place and off the visitor's connection.
* Keep `first` small: every hit costs one more request to render.

### A filtered, sorted listing with "load more"

Goal: the stories created since the start of the year, newest first, twenty at a time.

```ts
// src/app/stories/archive/actions.ts
'use server';
import { search } from '@uidu/client';
import { uidu } from '@/lib/uidu';

export async function storiesSince(from: string, after?: string) {
  const result = await search(uidu, {
    models: ['Story'],
    first: 20,
    after,
    params: {
      filters: [{ id: 'createdAt', kind: 'date', operator: '>=', value: from }],
      sorting: [{ id: 'createdAt', desc: true }],
    },
  });

  return {
    ids: (result?.edges ?? []).flatMap((e) => (e?.node ? [e.node.id] : [])),
    total: result?.totalCount ?? 0,
    // Pass back as `after` for the next twenty; null when there are no more.
    next: result?.pageInfo.hasNextPage ? (result.pageInfo.endCursor ?? null) : null,
  };
}

// storiesSince('2026-01-01')            → first page
// storiesSince('2026-01-01', page.next) → the next one
```

* Filter `id`s are the indexed field names in camelCase (`createdAt`, not `created_at`). A filter
  on a field the model doesn't index matches nothing.
* `=` is exact and case-sensitive (`Vanini` ≠ `vanini`). For "name contains", use `contains`, or
  put the text in `term`.
* With no `term`, the search matches everything that passes the filters — this is a plain listing,
  ordered by your `sorting`.

## API at a glance

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

| Function                                                                               | Kind | Auth         | CLI | Description                                                                                                                  |
| -------------------------------------------------------------------------------------- | ---- | ------------ | --- | ---------------------------------------------------------------------------------------------------------------------------- |
| [`search`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/search.ts) | read | public token | —   | Search the workspace index, optionally limited to `models`, with filters and sorting in `params`; page with `first`/`after`. |

**Types:** `SearchResult`

---

# Stories

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

> Fetch blog posts and long-form editorial content from the uidu Stories API.

* **Use it for:** [Public website](https://developers.uidu.org/docs/paths/public-website.md) · [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** public token — works in the browser
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu stories`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

A **Story** is uidu's blog and editorial primitive: a long-form piece with a title, a cover image, a
rich-text body and the usual publish metadata. Use it for a company blog, news posts, or any
"post-like" content that isn't a help-center article.

## Mental model

Stories hang directly off the workspace — no container to resolve first.

| Field                     | What it holds                                                                                         |
| ------------------------- | ----------------------------------------------------------------------------------------------------- |
| `name`                    | the title                                                                                             |
| `body`                    | Tiptap JSON — render it with [`<RichText>`](https://developers.uidu.org/docs/tools/react/components.md). Only `getStory` returns it |
| `cover`, `coverData`      | the cover image URL, and its metadata as JSON                                                         |
| `published`               | `true` once `publishedAt` is set and in the past                                                      |
| `publishedAt`             | ISO timestamp; a future one means the story is scheduled                                              |
| `publicPath`, `publicUrl` | the story's canonical permalink on uidu (`publicUrl` on `getStory` only)                              |

* `listStories` returns summaries (`id`, `name`, `cover`, `coverData`, `published`, `publishedAt`,
  `publicPath`) &#x2A;*without `body`**, so it's cheap. It's a flat array unwrapped from the Relay
  connection, one page at a time (`first` / `after`; `paginate` with `ListStoriesDocument` for all of
  them), newest-created first — **drafts included**: filter on `published` yourself.
* `getStory({ id })` takes the story's GlobalID **or its slug** (generated from the title), and
  returns the full `Story` with `body`, or `null`. An unknown slug can surface as a thrown
  `UiduError` rather than `null`, so a detail page should handle both.
* `createStory` and `updateStory` need the Bearer client. `createStory` returns `{ errors, story }`
  with `story` = `{ id, name }`; `updateStory` takes `{ input: { id, attributes } }` and returns the
  record at `story`. Validation failures come back in `errors`, not as a throw. The attributes
  include `name`, `body` (Tiptap JSON), `abstract`, `cover`, `publishedAt`, `slug`, `seoTitle`,
  `seoMetaDescription` and `storyTagList` — the full list is `StoryAttributes` in the
  [Schema](https://developers.uidu.org/docs/reference/schema.md).

## Use cases

### Blog index and post page

Goal: a `/stories` listing of what's published, newest first, and a page for each story.

```tsx
// src/app/stories/page.tsx
import Link from 'next/link';
import { listStories } from '@uidu/client';
import { uidu } from '@/lib/uidu';

export default async function StoriesPage() {
  const stories = await listStories(uidu, { first: 50 });
  const published = stories
    .filter((s) => s.published !== false)
    .sort((a, b) => String(b.publishedAt ?? '').localeCompare(String(a.publishedAt ?? '')));

  return (
    <ul>
      {published.map((s) => (
        <li key={s.id}>
          <Link href={`/stories/${encodeURIComponent(s.id)}`}>{s.name}</Link>
        </li>
      ))}
    </ul>
  );
}

// src/app/stories/[id]/page.tsx
import { notFound } from 'next/navigation';
import { getStory } from '@uidu/client';
import { RichText } from '@uidu/react';
import { uidu } from '@/lib/uidu';

export default async function StoryPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const story = await getStory(uidu, { id: decodeURIComponent(id) }).catch(() => null);
  if (!story || story.published === false) notFound();

  return (
    <article>
      {story.cover && <img src={story.cover} alt="" />}
      <h1>{story.name}</h1>
      {story.publishedAt && (
        <time dateTime={story.publishedAt as string}>
          {new Date(story.publishedAt as string).toLocaleDateString()}
        </time>
      )}
      <RichText doc={story.body} />
    </article>
  );
}
```

* The listing is sorted by `publishedAt` on your side: the API orders by creation date.
* A GlobalID contains `/` and `:`, hence `encodeURIComponent` in the link and `decodeURIComponent`
  in the page. Since `getStory` also accepts a slug, you can route on the slug instead if you keep
  one per story.
* `.catch(() => null)` folds "not found" and a failed request into a 404 — fine for a public page;
  log the error if you need to tell them apart.

### Publish a story from your server

Goal: an internal tool (or an import job) creates a story and publishes it.

```ts
// src/app/admin/stories/actions.ts
'use server';
import { createStory, updateStory } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin'; // the apiKey client — server-only

export async function publishStory(title: string, body: unknown) {
  const created = await createStory(uiduAdmin, {
    input: { attributes: { name: title, body } },
  });
  if (!created || created.errors?.length) {
    return { ok: false, errors: created?.errors?.map((e) => e.name) ?? [] };
  }

  const id = created.story!.id;
  const published = await updateStory(uiduAdmin, {
    input: { id, attributes: { publishedAt: new Date().toISOString() } },
  });
  if (published?.errors?.length) {
    return { ok: false, errors: published.errors.map((e) => e.name) };
  }
  return { ok: true, id };
}
```

* A story without `publishedAt` is a draft; setting it publishes it (a future date schedules it).
  You can also pass `publishedAt` straight to `createStory`.
* Never call these from the browser: the Bearer token would ship with it. The same from the
  terminal: `uidu stories create --attributes '{"name":"Our 2026 roadmap"}' --json`. The client
  set-up: [Server integration](https://developers.uidu.org/docs/paths/server-integration.md); the write contract:
  [Writes](https://developers.uidu.org/docs/foundations/writes.md).

## API at a glance

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

| Function                                                                                       | Kind  | Auth         | CLI              | Description                                                                                                                  |
| ---------------------------------------------------------------------------------------------- | ----- | ------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| [`createStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)   | write | Bearer       | `stories create` | Create a story; returns the payload as-is, `{ errors, story }`, validation failures included.                                |
| [`getStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)      | read  | public token | `stories get`    | One story with its rich-text `body`, or null.                                                                                |
| [`listStories`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)   | read  | public token | `stories list`   | The workspace's stories as a flat array of summaries (no `body`); page with `first`/`after`.                                 |
| [`updateStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `stories update` | Update a story: `{ input: { id, attributes } }` (create it with `createStory`). Returns the payload (with `errors`) or null. |

**Types:** `Story`

**CLI:**

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

---

# Spaces, tasks & notes

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

> Create, update and delete the workspace's Spaces, tasks and notes from a server or the CLI.

* **Use it for:** [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **Reads:** none
* **Writes:** Bearer (`apiKey`) — server-side only
* **CLI:** `uidu tasks`, `uidu notes`, `uidu spaces`
* **Auth:** what each token can do and where it may run — [Authentication](https://developers.uidu.org/docs/foundations/authentication.md)

**Spaces** are the areas a uidu workspace is organised into (a team, a project, a department);
**tasks** are the work inside them and **notes** the workspace's documents. The SDK covers them
for provisioning and automation: seed a new Space with its starter tasks, open a task from an
external system, file a note from a webhook.

## Mental model

```
Workspace
  ├─→ Space   (name, kind, color, icon, isPrivate, isLocked, isHidden)
  │     └─→ Task   (name, body, spaceId, statusId, priority, startDate / endDate,
  │                 parentId, goalId, workspaceTagIds, private, doneAt)
  └─→ Note    (name, body, icon, isPrivate, folderId)
```

* **Write-only today.** There are no `listSpaces` / `getSpace`, `listTasks` / `getTask` or
  `listNotes` / `getNote` functions yet. Keep the ids the create calls return — in your own
  database, next to the external record they came from — if you need to update or delete those
  records later.
* **Every function is authoring**: it needs an account Bearer token (`apiKey` on the client) and
  runs under the workspace's permissions. Use it server-side (route handler, server action,
  script) or through the [CLI](https://developers.uidu.org/docs/tools/cli.md) — never from the browser, and not from a
  [custom app](https://developers.uidu.org/docs/paths/custom-app.md), whose session token is refused for tasks and notes.
* **Rich text** fields (`body`, `description`) take a Tiptap/ProseMirror JSON document
  (`{ type: 'doc', content: [...] }`).
* **Tasks.** `priority` is `low`, `normal`, `high` or `urgent`; `startDate` / `endDate` are
  `YYYY-MM-DD`. `parentId` makes a subtask, `goalId` links the task to a
  [goal](https://developers.uidu.org/docs/domains/goals.md), `statusId` places it in a status of its Space. A task is finished
  either by setting `doneAt`, or by moving it to a status whose state is `completed` — uidu
  derives the task's state from its status.
* **Notes don't belong to a Space**: their attributes have no `spaceId`. They live in the
  workspace, optionally in a folder (`folderId`).

**Payload shapes differ.** Each function returns the mutation payload with `errors` rather than
throwing — always check it. Where the record sits:

| Function              | Record at                                         |
| --------------------- | ------------------------------------------------- |
| `createSpace`         | `space.node` — `{ id, name }`, wrapped in an edge |
| `createNote`          | `note.node` — `{ id }`, wrapped in an edge        |
| `createTask`          | `task` — `{ id, name }`, **not** wrapped          |
| `update*` / `delete*` | `space` / `task` / `note` — `{ id }`              |

## Use cases

### Seed a new Space with its starter tasks

Goal: a provisioning script that creates a project Space and fills it with a checklist, one
item with a subtask.

```ts
// scripts/seed-launch.ts — server-side
import { createClient, createSpace, createTask } from '@uidu/client';

const uiduAdmin = createClient({
  workspace: process.env.UIDU_WORKSPACE!,
  apiKey: process.env.UIDU_API_KEY!, // never shipped to a browser
});

const space = await createSpace(uiduAdmin, {
  input: { attributes: { name: 'Product launch', isPrivate: false, color: '#22c55e' } },
});
if (space?.errors?.length) throw new Error(space.errors.map((e) => e.name).join(', '));
const spaceId = space!.space!.node!.id; // createSpace wraps the space in an edge

const doc = (text: string) => ({
  type: 'doc',
  content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
});

const release = await createTask(uiduAdmin, {
  input: {
    attributes: {
      name: 'Draft the press release',
      spaceId,
      priority: 'high', // 'low' | 'normal' | 'high' | 'urgent'
      startDate: '2026-10-12',
      endDate: '2026-10-16',
      body: doc('One page, two quotes.'),
    },
  },
});
const releaseId = release?.task?.id; // createTask is not wrapped: task.id

await createTask(uiduAdmin, {
  input: { attributes: { name: 'Collect the quotes', spaceId, parentId: releaseId } },
});
```

* Rename or lock the Space later with `updateSpace(uiduAdmin, { input: { id: spaceId,
  attributes: { name: 'Launch — Q4', isLocked: true } } })`; remove it with
  `deleteSpace(uiduAdmin, { input: { id: spaceId } })`.
* The same from a terminal: `uidu spaces create --attributes '{"name":"Product launch"}'`, then
  `uidu tasks create --attributes '{"name":"…","spaceId":"…"}'` — every verb in
  [CLI commands](https://developers.uidu.org/docs/tools/cli/commands.md).

### Create tasks from an external system

Goal: a webhook from an issue tracker (or a form, or a CRM) opens a task in uidu, and closes it
when the external item is resolved.

```ts
// app/api/webhooks/tracker/route.ts
import { createTask, updateTask, deleteTask } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin'; // createClient({ workspace, apiKey })
import { db } from '@/lib/db'; // your own store: external id → uidu task id

export async function POST(req: Request) {
  const event = await req.json(); // verify the sender's signature first

  if (event.type === 'issue.opened') {
    const result = await createTask(uiduAdmin, {
      input: {
        attributes: {
          name: event.issue.title,
          spaceId: process.env.UIDU_SUPPORT_SPACE_ID,
          priority: event.issue.urgent ? 'urgent' : 'normal',
        },
      },
    });
    if (result?.errors?.length) {
      return Response.json({ errors: result.errors }, { status: 422 });
    }
    // no listTasks / getTask: this mapping is the only way back to the task
    await db.taskLinks.insert({ externalId: event.issue.id, taskId: result!.task!.id });
  }

  if (event.type === 'issue.resolved') {
    const link = await db.taskLinks.find(event.issue.id);
    if (link) {
      await updateTask(uiduAdmin, {
        input: { id: link.taskId, attributes: { doneAt: new Date().toISOString() } },
      });
    }
  }

  if (event.type === 'issue.deleted') {
    const link = await db.taskLinks.find(event.issue.id);
    if (link) await deleteTask(uiduAdmin, { input: { id: link.taskId } });
  }

  return new Response(null, { status: 204 });
}
```

* `UIDU_SUPPORT_SPACE_ID` and `db` stand for your own configuration and storage — the SDK
  doesn't provide them.
* Instead of `doneAt`, you can move the task with `statusId` to a status whose state is
  `completed`.

### File a note from a webhook

Goal: when a meeting tool posts a transcript summary, file it as a workspace note.

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

export async function fileMeetingNote(title: string, paragraphs: string[]) {
  const result = await createNote(uiduAdmin, {
    input: {
      attributes: {
        name: title,
        isPrivate: false,
        body: {
          type: 'doc',
          content: paragraphs.map((text) => ({
            type: 'paragraph',
            content: [{ type: 'text', text }],
          })),
        },
      },
    },
  });

  if (result?.errors?.length) return { ok: false, errors: result.errors };
  // like createSpace, the note comes wrapped in an edge — keep the id to edit it later
  return { ok: true, noteId: result!.note!.node!.id };
}
```

* Later edits: `updateNote(uiduAdmin, { input: { id: noteId, attributes: { name: 'Launch retro
  (final)' } } })`; `deleteNote(uiduAdmin, { input: { id: noteId } })` removes it. Both return
  `{ errors, note: { id } }`.

## API at a glance

Every `@uidu/client` function in **Spaces, tasks & notes**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#workspace) has the rest of the SDK.

| Function                                                                                       | Kind  | Auth   | CLI             | Description                                                                                                                 |
| ---------------------------------------------------------------------------------------------- | ----- | ------ | --------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [`createNote`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/notes.ts)      | write | Bearer | `notes create`  | Create a note. Returns the payload (with `errors`) or null; the new note is at `note.node`.                                 |
| [`createSpace`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/spaces.ts)    | write | Bearer | `spaces create` | Create a space, the container tasks live in. Returns the payload (with `errors`) or null; the new space is at `space.node`. |
| [`createTask`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/tasks.ts)      | write | Bearer | `tasks create`  | Create a task, usually in a space (`attributes.spaceId`). Returns the payload (with `errors`) or null.                      |
| [`deleteNote`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer | `notes delete`  | Delete a note by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                          |
| [`deleteSpace`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `spaces delete` | Delete a space by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                         |
| [`deleteTask`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer | `tasks delete`  | Delete a task by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                          |
| [`updateNote`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer | `notes update`  | Update a note: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                |
| [`updateSpace`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `spaces update` | Update a space: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                               |
| [`updateTask`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer | `tasks update`  | Update a task, e.g. its `statusId` or dates: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.  |

**CLI:**

| Entity   | list | get | create | update | delete |
| -------- | :--: | :-: | :----: | :----: | :----: |
| `tasks`  |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `notes`  |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `spaces` |   —  |  —  |    ✓   |    ✓   |    ✓   |

---

# Authentication

URL: https://developers.uidu.org/docs/foundations/authentication

> The three ways a request reaches uidu — public token, account Bearer, custom-app session — and where each may run.

What an app *looks like* — a landing page, a blog, a dashboard, a booking tool — doesn't decide
how it authenticates. **Who the request acts for** does:

| Acting for                                        | Token                      | `createClient` option | Runs                            |
| ------------------------------------------------- | -------------------------- | --------------------- | ------------------------------- |
| **Anyone** — public reads                         | public token               | `publicToken`         | anywhere, including the browser |
| **The workspace** — every write, and private data | account **Bearer**         | `apiKey`              | your server or the CLI only     |
| **The signed-in member** — an app inside uidu     | session from the host page | `fromBridge(bridge)`  | the browser, inside uidu only   |

One app can mix the rows: public pages rendered on the server with the public token, a contact
form submitted through a server action with the Bearer, and a members' area that only works when
uidu frames it.

Every [domain page](https://developers.uidu.org/docs/domains/cms.md) says at the top which token its reads and writes need, and
the [client reference](https://developers.uidu.org/docs/reference/client.md) says it per function.

## Public token — reads for anyone

Read-only access to what a workspace publishes: CMS pages, events, stories, forms, courses, donation
campaigns, the help center. It is **safe to ship in browser code**.

Get it from *Workspace → Settings → Public tokens*.

```ts
import { createClient } from '@uidu/client';

export const uidu = createClient({
  workspace: process.env.UIDU_WORKSPACE!,
  publicToken: process.env.UIDU_PUBLIC_TOKEN,
});
```

Private data — contacts and deals, people, goals, bookings, campaigns — isn't readable with it: the
query succeeds and returns nothing (an empty list, or `null`). If a list is unexpectedly empty, check
the domain's header before debugging the query.

## Bearer — the workspace

An account access token, sent as `Authorization: Bearer …`. It acts as the account it belongs to, so
it can do whatever that account's role in the workspace allows: read private data, and run **every
write** — including the visitor-facing ones (form responses, donations, RSVPs, job applications).

```ts
// lib/uidu.ts — imported only from server code
import { createClient } from '@uidu/client';

export const uidu = createClient({
  workspace: process.env.UIDU_WORKSPACE!,
  publicToken: process.env.UIDU_PUBLIC_TOKEN,
  apiKey: process.env.UIDU_API_KEY, // server-only
});
```

<Callout type="warn" title="Never in the browser">
  Keep `apiKey` on the server: a Server Component, a server action, a route handler, a script, or the
  CLI. Don't prefix it `NEXT_PUBLIC_`, and don't pass it to `<UiduProvider>` (which has no `apiKey`
  prop for this reason). A visitor submitting a form goes through a **server action** that calls
  `createFormResponse` with this client — see [Forms](https://developers.uidu.org/docs/domains/forms.md).
</Callout>

Get one from *Workspace → Settings → API keys*, or run [`uidu login`](https://developers.uidu.org/docs/tools/cli/login.md), which
signs you in through the browser and stores a token in `~/.uidu/config.json`. Set it as
`UIDU_API_KEY` where your server runs. Writes and their
`errors` are covered in [Writes](https://developers.uidu.org/docs/foundations/writes.md).

## Session — the signed-in member, inside uidu

A [custom app](https://developers.uidu.org/docs/paths/custom-app.md) runs in an iframe inside uidu. The host page hands it a
short-lived session (five minutes, refreshed for you) over `postMessage`: the app can do what that
member can do, and no more. There is no token to configure.

```tsx
'use client';
import { UiduAppProvider } from '@uidu/react';

// Inside, useUiduClient() / useUiduApp() give you a session-backed client.
<UiduAppProvider>{children}</UiduAppProvider>;
```

Outside React: `connect()` from `@uidu/app-bridge`, then `createClient(fromBridge(bridge))`.

Three rules follow from where the session lives:

1. **It only exists in the browser.** It arrives after the page loads, so Server Components and
   server actions never have it — pages that show member data are `'use client'`.
2. **It only works from the app's own origin.** Don't forward it to a backend of yours to call uidu
   from there.
3. **It reaches less than a Bearer.** The app's own [Models](https://developers.uidu.org/docs/domains/models.md), and read-only the
   workspace's [goals](https://developers.uidu.org/docs/domains/goals.md). Everything else comes back as an error for now.

## Choosing, in one line each

* Rendering public content (site, blog, events, careers): **public token**, on the server or in the browser.
* Anything that writes, or reads private data: **Bearer**, on the server.
* An app members open inside uidu: **session**, in the browser.

---

# Data model

URL: https://developers.uidu.org/docs/foundations/data-model

> How uidu's data is shaped — workspace, Sites and Models, the platform's own entities — and the conventions that repeat across all of them.

Internalize this once and every domain page reads the same way.

## The workspace

Everything lives in a **workspace** — one per organization, served at `https://{workspace}.uidu.org`,
with its own GraphQL endpoint. Tokens are workspace-scoped. Inside it there are two kinds of data:

* **The platform's own entities** — `Event`, `Story`, `Form`, `Course`, `DonationCampaign`,
  `Contact`, `Employee`, `Goal`… Each uidu app (Events, CRM, People…) owns some, they hang directly
  off the workspace, and each [domain page](https://developers.uidu.org/docs/domains/events.md) documents one family.
* **Models** — record types you define yourself, with typed Fields. They hold whatever uidu has no
  place for, and they are also what the CMS is built from.

## Sites and Models

```
Workspace
├── Site                          a website: domain, settings
│   ├── page-kind Model           "Page", "Landing" — a page type, with its fields
│   │   └── ModelItem             one page: slug, field values, ordered blocks
│   └── block-kind Model          "Hero", "Feature grid" — a block type
│       └── ModelItem             one block placed on a page
└── Model                         your own record type ("Booking", "Checklist")
    ├── Field                     name, kind, shortname
    └── ModelItem                 one record
        └── FieldValue            one field's value: content = { value: … }
```

* **Model** — a record type: a name and a list of **Fields** (`kind` such as `string`, `number`,
  `date`, `singleSelect`, `member`; a stable `shortname`).
* **ModelItem** — one record of a Model. Its values are **FieldValues**, read back keyed by shortname
  (`item.fieldValuesByShortname.room`).
* **Site** — the CMS. A site's page types and block types are Models; its pages and blocks are their
  items. That's why rendering a page and reading your own records look alike. See
  [CMS](https://developers.uidu.org/docs/domains/cms.md) for reading sites and [Models](https://developers.uidu.org/docs/domains/models.md) for your own types.

<Callout title="Read what the workspace has before creating a Model">
  A workspace is not empty: goals, contacts, deals, people, events are already there. Create a Model
  only for data uidu has no place for — copying existing records into one gives the organization a
  second copy that drifts from the real one.
</Callout>

### Legacy CMS: Projects

Sites already deployed on the older CMS keep working:

```
Workspace
└── Project                       "marketing site", "help center"
    └── Page                      identified by slug
        └── PageBlock             one section, typed by its templateBlock
            └── FieldValue        one piece of data, typed by its Field
```

Projects can be migrated to Sites from the CMS app. New sites should start on Sites.

## Naming: use `name`

Display names come from the entity's `name` field. It's user-set and may be `null` on drafts — render
a fallback when that happens:

```tsx
<h1>{event.name ?? 'Untitled event'}</h1>
```

A few entities have no `name` field (they're transactional or derived):

| Entity          | What to show instead                         |
| --------------- | -------------------------------------------- |
| `Booking`       | `${kind} #${number}` or `contact.name`       |
| `Enrollment`    | `course.name` + state                        |
| `EmailCampaign` | `subject`                                    |
| `Employee`      | `user.name` (or `user.firstName + lastName`) |

<Callout type="warn" title="Don't use humanName">
  The schema exposes a `humanName` field on most types. It's **not** a display name — it's the Rails
  `model_name.human` (the humanized class label like "Booking" or "Calendar event"). Useful for
  `__typename`-style disambiguation, never for the title of a record.
</Callout>

## Rich text: `body` is Tiptap JSON

Rich-text fields (`Event.body`, `Call.body`, `Course.body`, CMS block bodies, `CalendarEvent.body`,
`Lecture.body`, `KbArticle.body`) are stored as **Tiptap/ProseMirror JSON** — a tree of
`{ type, content, marks }` nodes, typed `JSON` in the schema. Render them with
[`<RichText>`](https://developers.uidu.org/docs/tools/react/components.md):

```tsx
import { RichText } from '@uidu/react';

<RichText doc={event.body} />
```

## Money: amounts are in cents

Money values (`DonationCampaign.goal`, `Donation.amount`, `Deal.amount`, product prices, booking
totals) are integers in the smallest unit of the currency — cents for EUR/USD. Divide by 100 to
display:

```tsx
const eur = (campaign.goal ?? 0) / 100;
<span>€{eur.toLocaleString()}</span>
```

The exception is a job's salary range (`Call.salaryMin` / `salaryMax`), which is in whole units —
[`formatSalaryRange`](https://developers.uidu.org/docs/domains/jobs.md) formats it.

## Lists, field values, errors

Three more rules apply everywhere and have their own pages:

* `list*` functions return one page, flattened — [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md) covers reading everything.
* A field value's `content` is always `{ value: … }` — same page.
* Functions throw `UiduError` on transport and GraphQL failures; writes report validation problems in
  `errors` instead — [Errors](https://developers.uidu.org/docs/foundations/errors.md) and [Writes](https://developers.uidu.org/docs/foundations/writes.md).

<Callout type="info" title="Schema-typed everywhere">
  Arguments and return values are typed from the live schema via codegen. If a field is nullable in the
  schema, it's nullable in TypeScript.
</Callout>

## API at a glance

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

| Function                                                                                                                  | Kind   | Auth | CLI | Description                                                                                                                                       |
| ------------------------------------------------------------------------------------------------------------------------- | ------ | ---- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createClient`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/client.ts)                              | helper | —    | —   | Build a `UiduClient`; it needs a `workspace` or an `endpoint`, and authenticates with `publicToken`, `apiKey` or `getToken`.                      |
| [`fromBridge`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/client.ts)                                | helper | —    | —   | Client config for a custom app, from a connected `@uidu/app-bridge`: the bridge's GraphQL endpoint, with its session token asked for per request. |
| [`normalizeFieldValueContent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/fieldValues.ts)     | helper | —    | —   | Wrap a `content` payload in `{ value }` unless it already carries a `value` key. Returns the input untouched when there is nothing to do.         |
| [`normalizeFieldValuesAttributes`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/fieldValues.ts) | helper | —    | —   | Normalize a standalone `fieldValuesAttributes` array. Exported for callers that assemble one by hand outside an SDK mutation.                     |
| [`paginate`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/paginate.ts)                          | read   | —    | —   | Walks every page of a Relay-style connection and returns a flat array of nodes.                                                                   |

**Types:** `PaginateOptions`, `UiduClientConfig`, `UiduErrorOptions`

---

# Errors

URL: https://developers.uidu.org/docs/foundations/errors

> How to handle UiduError and the underlying GraphQL/network failure modes.

Every SDK function throws `UiduError` on failure — no exceptions slip through untyped.

## The shape

```ts
import { UiduError } from '@uidu/client';

class UiduError extends Error {
  readonly code: string; // one of the codes below
  readonly graphQLErrors?: ReadonlyArray<GraphQLError>;
  readonly cause?: unknown;
}
```

## When to use which code

| Code            | Cause                                                                                                                                                                                                        |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GRAPHQL_ERROR` | The server returned a 200 OK with `errors[]` in the body. Typical reasons: schema validation failed, the requested entity is restricted by permissions, or a resolver crashed. `graphQLErrors` is populated. |
| `NETWORK_ERROR` | The request never got a useful HTTP response — DNS, TLS, timeout, connection reset, CORS preflight failure. `cause` holds the underlying transport error.                                                    |
| `CONFIG_ERROR`  | `createClient` was given neither a `workspace` nor an `endpoint`. Thrown when the client is built, not per request.                                                                                          |
| `TOKEN_ERROR`   | The client's `getToken` failed — in a custom app, the bridge couldn't produce a session token. `cause` holds the original error.                                                                             |

## Handling pattern

```ts
import { getPage, UiduError } from '@uidu/client';

try {
  const page = await getPage(client, { projectId, slug });
  return page;
} catch (err) {
  if (err instanceof UiduError) {
    if (err.code === 'GRAPHQL_ERROR') {
      // Server-side reasoning. Log err.graphQLErrors for the details.
      console.warn('Schema error:', err.graphQLErrors);
      return null;
    }
    if (err.code === 'NETWORK_ERROR') {
      // Retry, or surface a "service unavailable" page.
      throw err;
    }
  }
  throw err;
}
```

## Don't swallow

A common mistake: wrap every SDK call in `try/catch (_) {}` and return `null`. That hides server-side problems (a tags field crashing the resolver, a permission gate triggering a 500). Let `NETWORK_ERROR` bubble; only swallow `GRAPHQL_ERROR` if you know the schema can validly reject the request (e.g. a missing slug).

## Inspecting the underlying GraphQL errors

```ts
catch (err) {
  if (err instanceof UiduError && err.code === 'GRAPHQL_ERROR') {
    for (const e of err.graphQLErrors ?? []) {
      console.log(e.message);
      console.log(e.path);
      console.log(e.extensions); // server-side error metadata
    }
  }
}
```

<Callout type="info">
  If a query fails with no error body (empty `errors[]` and the field nulled out), it's typically a Rails-side resolver crash. Drop the offending field from the selection and file a server-side bug.
</Callout>

---

# Installation

URL: https://developers.uidu.org/docs/foundations/installation

> Install @uidu/client and configure tokens.

## Install the packages

```bash
npm install @uidu/client
# or
yarn add @uidu/client
# or
pnpm add @uidu/client
```

If you're using React, also install the bindings:

```bash
npm install @uidu/react
```

Both packages publish `"sideEffects": false` and are fully tree-shakable — only the functions you import end up in your bundle.

## Configure tokens

A site needs a **public token** (*Workspace → Settings → Public tokens*, safe in the browser) and,
for anything that writes or reads private data, an **API key** (*Workspace → Settings → API keys*,
server only). [Authentication](https://developers.uidu.org/docs/foundations/authentication.md) explains which to use where.

A typical `.env`:

```bash
UIDU_WORKSPACE=acme
UIDU_PUBLIC_TOKEN=pk_live_...
UIDU_API_KEY=sk_live_...     # server only
UIDU_PROJECT_ID=Z2lkOi8v...   # legacy Projects CMS only
```

## Verify the install

```ts
import { createClient } from '@uidu/client';

const client = createClient({
  workspace: process.env.UIDU_WORKSPACE!,
  publicToken: process.env.UIDU_PUBLIC_TOKEN!,
});

const ping = await fetch(client.endpoint).then((r) => r.status);
console.log(ping); // 200 if reachable, 401 if token is wrong
```

<Callout type="info">
  The endpoint resolves to `https://{workspace}.uidu.org/graphql` by default. Override with the `endpoint` option for local Rails dev (`http://localhost:3000/graphql`).
</Callout>

Continue with the [quickstart](https://developers.uidu.org/docs/paths/public-website.md).

---

# Pagination & field values

URL: https://developers.uidu.org/docs/foundations/pagination-and-field-values

> Reading whole collections — list functions, paginate, useQuery — and the one shape every field value's content takes.

Two rules that apply to every domain.

## Lists are Relay connections

Every list in the GraphQL API is a Relay connection: `{ edges: [{ node }], pageInfo: { hasNextPage, endCursor } }`.
How much of that you see depends on how you read it:

| You call                                      | You get                              | Use it for                                |
| --------------------------------------------- | ------------------------------------ | ----------------------------------------- |
| `listEvents(client, { first: 20 })`           | one page, **flattened** to `Event[]` | "the latest 20" — most pages              |
| `paginate(client, ListEventsDocument, …)`     | **every** page, flattened            | sitemaps, `generateStaticParams`, exports |
| `useQuery(ListEventsDocument, { first: 20 })` | one page, **raw** — `edges[].node`   | Client Components                         |

### One page — `list*`

`list*` functions take `first` (page size) and, where the query supports it, `after` (a cursor) plus
domain filters. They return the nodes only, so they can't tell you whether there is a next page — when
you need that, use `paginate` or the raw connection.

```ts
import { listStories } from '@uidu/client';

const latest = await listStories(client, { first: 10 });
```

### Everything — `paginate`

`paginate` walks the connection for you, threading `endCursor` into `after` until `hasNextPage` is
false, and returns one flat array. Pass the generated `*Document` for the list and tell it where the
connection sits in the result:

```ts
import { paginate, ListEventsDocument } from '@uidu/client';

const events = await paginate(client, ListEventsDocument, {
  variables: { first: 100 },
  extract: (data) => data.currentWorkspace?.events,
});
```

`maxPages` (default 1000) bounds the loop. Every list document is exported from `@uidu/client` — the
[client reference](https://developers.uidu.org/docs/reference/client.md) lists them per domain. Worked examples: [Paginate every
page](https://developers.uidu.org/docs/recipes/paginate.md), [Dynamic CMS routes](https://developers.uidu.org/docs/recipes/dynamic-cms-routes.md).

### In a Client Component — `useQuery`

[`useQuery`](https://developers.uidu.org/docs/tools/react/hooks.md) runs the same documents through SWR and returns what the query
returns, connection included:

```tsx
const { data } = useQuery(ListEventsDocument, { first: 20 });
const events = data?.currentWorkspace?.events?.edges?.map((e) => e?.node) ?? [];
const more = data?.currentWorkspace?.events?.pageInfo?.hasNextPage;
```

## A field value's `content` is always `{ value: … }`

Field values carry the data of CMS blocks, form responses, Model items, and the custom fields of
contacts, bookings or stories. Whenever you **write** one, its `content` is an object with a `value`
key — never the bare value:

```ts
// ✅ every value wrapped
fieldValuesAttributes: [
  { fieldId: 'f1', content: { value: 'hello@example.com' } },
  { fieldId: 'f2', content: { value: 42 } },
  { fieldId: 'f3', content: { value: ['gid://…', 'gid://…'] } }, // multipleSelect
];

// ❌ silently loses the answer
fieldValuesAttributes: [{ fieldId: 'f1', content: 'hello@example.com' }];
```

It holds for every field kind (`{ value: true }`, `{ value: '2026-07-27' }`, `{ value: <doc> }` for
rich text) and wherever `fieldValuesAttributes` appears.

<Callout type="warn" title="A flat value fails quietly">
  The API doesn't reject a bare value: the record saves, its typed columns stay empty, and reading that
  field later errors. `@uidu/client` wraps a bare `content` for you on every mutation and warns in
  development — but other API clients won't, so send the right shape.
</Callout>

If you assemble `fieldValuesAttributes` by hand outside an SDK mutation, `normalizeFieldValuesAttributes`
(or `normalizeFieldValueContent` for one value) applies the same fix. For Model items,
`toFieldValuesAttributes(model, { room: 'Blu' })` builds the array from shortnames — see
[Models](https://developers.uidu.org/docs/domains/models.md).

Reading is simpler: [`useFields`](https://developers.uidu.org/docs/tools/react/hooks.md) turns a CMS block's `fieldValues` into a
`{ shortname: value }` map, and Model items come with `fieldValuesByShortname`.

---

# Writes

URL: https://developers.uidu.org/docs/foundations/writes

> How create / update / delete work in @uidu/client and the uidu CLI — auth, input shape, error handling, and the per-entity capability matrix.

Beyond reading, `@uidu/client` exposes **authoring** functions — `createX` / `updateX` /
`deleteX` — for provisioning and editing workspace content. The `uidu` CLI wraps the same
functions as `<entity> create|update|delete`.

## Auth: every write needs a Bearer token

Writes authenticate as an account (`apiKey`), so they run on your server — a server action, route
handler, RSC or script — or from the CLI, never in the browser. That includes the visitor-facing
ones: a form response or a donation is submitted from a server action. See
[Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## Input shape

Every mutation takes a single `input` object. `create` carries `attributes`; `update` adds the
`id`; `delete` takes just the `id`:

```ts
import { createCourse, updateCourse, deleteCourse } from '@uidu/client';

await createCourse(client, { input: { attributes: { name: 'Onboarding' } } });
await updateCourse(client, { input: { id, attributes: { name: 'Onboarding v2' } } });
await deleteCourse(client, { input: { id } });
```

From the CLI:

```bash
uidu courses create --name "Onboarding"
uidu courses update <id> --attributes '{"name":"Onboarding v2"}'
uidu courses delete <id>
```

## Always check `errors`

Mutations resolve to a payload with an `errors` array and the affected record. A &#x2A;*non-empty
`errors`** means a validation failure — the record is unchanged.

```ts
const result = await createCourse(client, { input: { attributes: { name: 'Onboarding' } } });
if (result?.errors?.length) {
  // handle validation messages (result.errors[].name)
} else {
  result?.course?.id;
}
```

The CLI exits non-zero and prints the errors when the array is non-empty.

## Capability matrix

Not every entity supports every verb — this reflects what the client and CLI expose today,
generated from the CLI's resource registry. Beyond these, the CMS (`page`, `block`, `field`)
and provisioning (`workspace`, `project`) commands have their own verbs — see
[CLI commands](https://developers.uidu.org/docs/tools/cli/commands.md) — and any tool-flagged backend action can be run with
`executeAction` / `uidu tools call <key>`.

| Entity                | list | get | create | update | delete |
| --------------------- | :--: | :-: | :----: | :----: | :----: |
| `events`              |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
| `attendances`         |   —  |  —  |    ✓   |    —   |    —   |
| `stories`             |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
| `donations`           |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `courses`             |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `forms`               |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `contacts`            |   ✓  |  ✓  |    ✓   |    —   |    ✓   |
| `deals`               |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
| `goals`               |   ✓  |  ✓  |    —   |    ✓   |    —   |
| `timeframes`          |   ✓  |  —  |    —   |    —   |    —   |
| `employees`           |   ✓  |  —  |    ✓   |    ✓   |    —   |
| `employments`         |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `offices`             |   ✓  |  —  |    —   |    —   |    —   |
| `roles`               |   ✓  |  —  |    —   |    —   |    —   |
| `ccnls`               |   ✓  |  —  |    ✓   |    —   |    —   |
| `bookings`            |   ✓  |  ✓  |    —   |    —   |    —   |
| `calls`               |   ✓  |  ✓  |    —   |    —   |    —   |
| `jobs`                |   ✓  |  ✓  |    —   |    —   |    —   |
| `applications`        |   ✓  |  —  |    ✓   |    —   |    —   |
| `campaigns`           |   ✓  |  ✓  |    —   |    —   |    —   |
| `kb-collections`      |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `kb-articles`         |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `channel`             |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `time-clocks`         |   —  |  —  |    ✓   |    —   |    —   |
| `compensations`       |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `tasks`               |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `benefits`            |   —  |  —  |    ✓   |    —   |    —   |
| `benefit-enrollments` |   —  |  —  |    ✓   |    —   |    —   |
| `notes`               |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `projects`            |   ✓  |  —  |    —   |    —   |    —   |
| `sites`               |   —  |  —  |    ✓   |    —   |    —   |
| `spaces`              |   —  |  —  |    ✓   |    ✓   |    ✓   |

See the [CLI commands](https://developers.uidu.org/docs/tools/cli/commands.md) for the read verbs too, and
[Schema](https://developers.uidu.org/docs/reference/schema.md) for the `*Attributes` input fields per entity.

## API at a glance

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

| Function                                                                                                        | Kind  | Auth   | CLI                     | Description                                                                                                                                                 |
| --------------------------------------------------------------------------------------------------------------- | ----- | ------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createWorkspace`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/system.ts)                 | write | Bearer | `workspace create`      | Create a workspace owned by the authenticated account (`attributes.name`, `subdomain`…). Returns the payload (with `errors`) or null.                       |
| [`executeAction`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/actions.ts)                  | write | Bearer | `tools call`            | Run any backend action by `key` (an `ActionKey` flagged `of_kind_tool_key`), with optional `id` and `attributes`; returns `{ result, errors }` or null.     |
| [`generateWorkspaceApiCredentials`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/system.ts) | write | Bearer | `workspace credentials` | Rotate + return a workspace's API credentials (apiKey/apiSecret). Operates on the current workspace. Use the returned credentials to wire a scaffolded app. |

---

# Custom app

URL: https://developers.uidu.org/docs/paths/custom-app

> Build an app that uidu shows inside a Space, acting as the signed-in member — from the template to your own data in Models.

A **custom app** is a web app you host yourself that uidu shows inside a Space (or the workspace) in
a sandboxed iframe. uidu hands it a short-lived session for the member looking at it, so the app reads
and writes as that person — no login screen, no token to configure, nothing secret in the browser.

By the end of this path you'll have an app that connects to uidu, knows who is looking, reads the
workspace's goals, and keeps its own records in a Model.

### Scaffold from the template

```bash
npm create uidu-app@latest my-app -- -t custom-app
```

You get a Next.js app with `<UiduAppProvider>` already wired, a screen for when it's opened outside
uidu, the page's theme and accent colour applied, `public/uidu.app.json` to declare the app's Models,
and a `Content-Security-Policy: frame-ancestors` header so only uidu can frame it. See
[create-uidu-app](https://developers.uidu.org/docs/tools/create-uidu-app.md).

### Connect to the page framing you

Everything that touches member data sits under `<UiduAppProvider>`, in client components:

```tsx
// src/components/app-provider.tsx — rendered by app/layout.tsx around {children}
'use client';
import { UiduAppProvider } from '@uidu/react';
import { DEFAULT_HOST_ORIGINS } from '@uidu/app-bridge';

export function AppProvider({ children }: { children: React.ReactNode }) {
  // https://*.uidu.org by default; spread it to add a local uidu or a custom domain
  return <UiduAppProvider hostOrigins={[...DEFAULT_HOST_ORIGINS]}>{children}</UiduAppProvider>;
}
```

The session only exists in the browser, after the page loads: a Server Component or a server action
never has it, so pages that show member data are `'use client'`. Why, and the other rules of the
session: [Authentication](https://developers.uidu.org/docs/foundations/authentication.md#session--the-signed-in-member-inside-uidu).

### Know who is looking

`useUiduApp()` gives you the connection state, a session-backed client, and the context uidu sent:

```tsx
'use client';
import { useUiduApp } from '@uidu/react';

export function Hello() {
  const app = useUiduApp(); // 'connecting' | 'ready' | 'error'

  if (app.status === 'connecting') return <p>Connecting…</p>;
  if (app.status === 'error') return <p>Open this app from uidu ({app.error.code})</p>;

  const { user, space, locale, theme } = app.context;
  return <p>Hi {user?.name} — {space?.name ?? 'whole workspace'}</p>;
}
```

`context` also carries `workspaceApp` (the installed app — the owner of its Models) and `accent`, the
page's colour to set as `--primary`. Every field and state: [App bridge](https://developers.uidu.org/docs/tools/app-bridge.md).

### Read what the workspace already has

A workspace isn't empty, and the session can read some of it. Today that's the workspace's
[goals](https://developers.uidu.org/docs/domains/goals.md) — those of the workspace and of the Space the app sits in:

```ts
import { listGoals, isGoalBehind } from '@uidu/client';

const goals = await listGoals(app.client);
const behind = goals.filter((g) => !g.parentId && isGoalBehind(g, { tolerance: 0.1 }));
```

Everything else — contacts, deals, people, events, tasks — is refused to a session for now. When the
data exists in uidu but the app can't reach it yet, say so to the person rather than copying it into a
Model: the copy drifts from the real records.

### Keep your own data in a Model

For data uidu has no place for — a room booking, a checklist — define a [Model](https://developers.uidu.org/docs/domains/models.md)
and store items in it. They stay searchable and permissioned inside uidu.

```ts
import { createModelItem, ensureModel, listModelItems, toFieldValuesAttributes } from '@uidu/client';

const model = await ensureModel(app.client, {
  workspaceAppId: app.context.workspaceApp.id,
  name: 'Booking',
  fields: [{ shortname: 'room', name: 'Room', kind: 'string' }],
});

const created = await createModelItem(app.client, {
  input: {
    attributes: {
      modelId: model.id,
      fieldValuesAttributes: toFieldValuesAttributes(model, { room: 'Blu' }),
    },
  },
});

const items = await listModelItems(app.client, { modelId: model.id }); // item.fieldValuesByShortname.room
```

<Callout title="Two things that bite">
  **Call `ensureModel` once per app instance** (memoize the promise): two concurrent calls each create
  the model. And `listModelItems` reads uidu's search index, which catches up a moment after a write —
  after `createModelItem` / `deleteModelItem`, update your list from the mutation payload instead of
  listing again.
</Callout>

### Handle being opened outside uidu

Opened directly in a tab, the app has no host to connect to: `useUiduApp()` ends in `'error'` with
`NOT_EMBEDDED`. The template shows a "open this from uidu" screen; the
[demo](https://github.com/uidu-org/api.js/tree/main/apps/demo/src/app/custom-app) falls back to
`localStorage`. Don't use cookies or `localStorage` for shared state inside uidu — in a cross-origin
iframe cookies are third-party (Safari drops them) and `localStorage` is one browser only.

### Ship it

Deploy the app anywhere that serves HTTPS, then add it to a Space in uidu. Keep two lists in step: the
`hostOrigins` you give the bridge and the `frame-ancestors` header — the template builds both from
`https://*.uidu.org` plus `NEXT_PUBLIC_UIDU_HOST_ORIGINS`. If uidu runs on your own domain, add it to
that variable.

## Where to go next

* [App bridge](https://developers.uidu.org/docs/tools/app-bridge.md) — `connect()`, error codes, origins, the protocol, `fromBridge`.
* [Models](https://developers.uidu.org/docs/domains/models.md) — fields, field options, updating and deleting items.
* [Goals](https://developers.uidu.org/docs/domains/goals.md) — values stored ×100, progress helpers.
* [Authentication](https://developers.uidu.org/docs/foundations/authentication.md) — how the session differs from the other two tokens.

---

# Public website

URL: https://developers.uidu.org/docs/paths/public-website

> Build a public site on uidu, from an empty folder to CMS pages, a contact form and an events section, rendered statically.

Use this path when you're building something **anyone can visit**: a landing page, an
organisation's site, a blog, an events calendar. The pages read public content with the public
token. The one thing a visitor writes, a contact form, goes through a server action that holds the
workspace's Bearer token.

By the end you'll have a Next.js site that renders pages from a uidu **Site**, takes contact-form
submissions, lists events with a page for each one, and pre-renders all of it at build time.

### Scaffold the app

```bash
npm create uidu-app@latest my-site
```

The default `minimal` template gives you Next.js 16 (App Router), Tailwind, a configured
`src/lib/uidu.ts` and an `.env.local` filled in from the prompts. Its home page reads a **legacy CMS
Project** with `<PageBlocks>` and asks for `UIDU_PROJECT_ID` and `UIDU_LANDING_SLUG`. Step 3
replaces that page with one that reads a Site, so you can leave those two empty. Other templates and
every flag: [create-uidu-app](https://developers.uidu.org/docs/tools/create-uidu-app.md).

Already have a Next.js app? Add the packages instead: `npm install @uidu/client @uidu/react swr`
(see [Installation](https://developers.uidu.org/docs/foundations/installation.md)), then write the file in step 2 yourself.

### Create the client

One client for the whole site. It reads with the public token and writes with the API key, so
import it only from server code: Server Components, server actions, `generateStaticParams`.

```ts
// src/lib/uidu.ts
import { createClient } from '@uidu/client';

export const uidu = createClient({
  workspace: process.env.UIDU_WORKSPACE ?? '',
  publicToken: process.env.UIDU_PUBLIC_TOKEN,
  apiKey: process.env.UIDU_API_KEY, // server-only
});
```

Which token does what, where you get each one, and why `apiKey` never reaches the browser:
[Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

### Render a page from your Site

A **Site** holds your pages. Each page has its own field values and an ordered list of **blocks**,
and each block has a kind (a block-kind Model) and its own `fields`, keyed by field shortname. Find
the Site by the domain it's served on, get a page by slug, and render each block with the component
for its kind. You write that component map yourself: `<PageBlocks>` from `@uidu/react` reads legacy
Project pages, not Sites.

```tsx
// src/lib/site.ts
import { cache } from 'react';
import { getSiteByDomain } from '@uidu/client';
import { uidu } from './uidu';

// The domain the Site is served on, as set in uidu.
export const getSite = cache(() => getSiteByDomain(uidu, { domain: 'www.acme.org' }));

// src/components/site-blocks.tsx
import type { ComponentType } from 'react';
import type { SiteBlock } from '@uidu/client';
import { RichText, toText } from '@uidu/react';

function Hero({ block }: { block: SiteBlock }) {
  return (
    <section>
      <h1>{toText(block.fields.title)}</h1>
      <RichText doc={block.fields.body} />
    </section>
  );
}

// Keyed by the block kind's name in uidu.
const components: Record<string, ComponentType<{ block: SiteBlock }>> = { Hero };

export function SiteBlocks({ blocks }: { blocks: SiteBlock[] }) {
  return blocks.map((block) => {
    const Component = components[block.model.name ?? ''];
    return Component ? <Component key={block.id} block={block} /> : null;
  });
}

// src/app/page.tsx
import { getPageBySlug } from '@uidu/client';
import { notFound } from 'next/navigation';
import { SiteBlocks } from '@/components/site-blocks';
import { getSite } from '@/lib/site';
import { uidu } from '@/lib/uidu';

export default async function HomePage() {
  const site = await getSite();
  if (!site) notFound();

  const page = await getPageBySlug(uidu, { siteId: site.id, slug: 'home' });
  if (!page) notFound();

  return <SiteBlocks blocks={page.blocks} />;
}
```

The block map is keyed on `block.model.name` because the API doesn't expose a block kind's
shortname yet. Pages, singletons such as the header and footer, drafts and previews:
[CMS](https://developers.uidu.org/docs/domains/cms.md).

### Add a contact form

Build the form in uidu, then get it with `getForm` and render it with `<DynamicForm>`, which picks an
input for each question and hands the answers to your `action`. The action is a **server action**:
like every write, `createFormResponse` needs the client with the API key. Every answer's `content`
is `{ value }`.

```tsx
// src/app/contact/page.tsx
import { createFormResponse, getForm } from '@uidu/client';
import { DynamicForm, type DynamicFormResult, type DynamicFormValues } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

const FORM_ID = 'Z2lkOi8v…'; // from the Forms app, or `uidu forms list`

export default async function ContactPage() {
  const form = await getForm(uidu, { id: FORM_ID });
  if (!form) notFound();

  async function submit(values: DynamicFormValues): Promise<DynamicFormResult> {
    'use server';
    const result = await createFormResponse(uidu, {
      input: {
        attributes: {
          formId: FORM_ID,
          fieldValuesAttributes: values.fieldValues.map((fv) => ({
            fieldId: fv.fieldId,
            content: { value: fv.value },
          })),
          contactAttributes: values.contact && {
            email: values.contact.email,
            contactableAttributes: {
              kind: 'person',
              firstName: values.contact.firstName,
              lastName: values.contact.lastName || undefined,
            },
          },
        },
        event: 'complete!',
      },
    });

    if (result?.errors?.length) return { ok: false, errors: result.errors };
    return { ok: true, meta: { responseId: result?.formResponse?.id } };
  }

  return <DynamicForm form={form} action={submit} submitLabel="Send" />;
}
```

Validation failures come back in `errors`, not as a throw; `<DynamicForm>` shows them above the
button. The input shape and drafts: [Forms](https://developers.uidu.org/docs/domains/forms.md). The component's props:
[React components](https://developers.uidu.org/docs/tools/react/components.md). Errors on every write:
[Writes](https://developers.uidu.org/docs/foundations/writes.md).

### List events, with a page for each

A listing with `listEvents` and a detail page with `getEvent`. Put the detail route under a scope
path, `/event/[id]`, not at the root: a root-level `/[id]` also catches requests like
`/favicon.ico` and turns them into failed queries.

```tsx
// src/app/events/page.tsx
import { listEvents } from '@uidu/client';
import Link from 'next/link';
import { uidu } from '@/lib/uidu';

export default async function EventsPage() {
  const events = await listEvents(uidu, { first: 50 });

  return (
    <ul>
      {events.map((event) => (
        <li key={event.id}>
          <Link href={`/event/${encodeURIComponent(event.id)}`}>{event.name}</Link>
        </li>
      ))}
    </ul>
  );
}

// src/app/event/[id]/page.tsx
import { getEvent } from '@uidu/client';
import { RichText } from '@uidu/react';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';

export default async function EventPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const event = await getEvent(uidu, { id: decodeURIComponent(id) });
  if (!event) notFound();

  return (
    <article>
      <h1>{event.name}</h1>
      {event.instance?.beginsAt && (
        <time>{new Date(event.instance.beginsAt as string).toLocaleString()}</time>
      )}
      <RichText doc={event.body} />
    </article>
  );
}
```

Instances, addresses, calendar links and RSVPs with `createAttendance`: [Events](https://developers.uidu.org/docs/domains/events.md).

### Render every page at build time

`generateStaticParams` tells Next.js which pages to pre-render. `listSitePages` already returns all
of a Site's published pages; events come one page at a time, so walk them all with `paginate` and
the generated `ListEventsDocument`.

```tsx
// src/app/[slug]/page.tsx — every Site page except the home page
import { getPageBySlug, listSitePages } from '@uidu/client';
import { notFound } from 'next/navigation';
import { SiteBlocks } from '@/components/site-blocks';
import { getSite } from '@/lib/site';
import { uidu } from '@/lib/uidu';

export const dynamicParams = false; // only the slugs below; anything else is a 404

export async function generateStaticParams() {
  const site = await getSite();
  if (!site) return [];
  const pages = await listSitePages(uidu, { siteId: site.id });
  return pages.flatMap((p) => (p.slug && p.slug !== 'home' ? [{ slug: p.slug }] : []));
}

export default async function SitePage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const site = await getSite();
  const page = site && (await getPageBySlug(uidu, { siteId: site.id, slug }));
  if (!page) notFound();
  return <SiteBlocks blocks={page.blocks} />;
}

// src/app/event/[id]/page.tsx — add next to the page component from step 5
import { paginate, ListEventsDocument } from '@uidu/client';

export async function generateStaticParams() {
  const events = await paginate(uidu, ListEventsDocument, {
    variables: { first: 100 },
    extract: (data) => data.currentWorkspace?.events,
  });
  return events.map((event) => ({ id: event.id }));
}
```

`/` stays with `app/page.tsx` from step 3. With `dynamicParams = false` the root `[slug]` route only answers the slugs it was built with, so it
doesn't catch stray requests. Events keep the default: one published after the build still renders
on its first visit. How `paginate` walks a connection: [Pagination & field
values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md). A sitemap and revalidation on top of the same
route: [Dynamic CMS routes](https://developers.uidu.org/docs/recipes/dynamic-cms-routes.md).

### Deploy

Set the same three variables wherever the site builds and runs:

```bash
UIDU_WORKSPACE=acme
UIDU_PUBLIC_TOKEN=...   # public reads
UIDU_API_KEY=...        # the contact form's server action; server-only
```

They're needed **at build time** too, because `generateStaticParams` and the static pages query
uidu during the build. Keep `UIDU_API_KEY` a plain server variable: never prefix it `NEXT_PUBLIC_`.

## Where to go next

* **More public content** — [Stories](https://developers.uidu.org/docs/domains/stories.md) for a blog, [Donations](https://developers.uidu.org/docs/domains/donations.md)
  for campaigns and a donate form, [Help center](https://developers.uidu.org/docs/domains/help-center.md) for an FAQ,
  [Jobs](https://developers.uidu.org/docs/domains/jobs.md) for a careers page.
* **Recipes** — [Multi-step forms](https://developers.uidu.org/docs/recipes/multi-step-forms.md), [File upload](https://developers.uidu.org/docs/recipes/file-upload.md),
  [Search as you type](https://developers.uidu.org/docs/recipes/search-as-you-type.md), and the [demo app](https://developers.uidu.org/docs/recipes/demo-app.md),
  a full Next.js site built this way.
* **Reference** — every function in the [client reference](https://developers.uidu.org/docs/reference/client.md), every component and
  hook in the [React reference](https://developers.uidu.org/docs/tools/react/components.md).

---

# Server integration

URL: https://developers.uidu.org/docs/paths/server-integration

> Read and write a workspace's private data from your own server — internal tools, sync jobs, provisioning.

Use this path when your code runs on a server you control and acts **as the workspace**: an
internal dashboard over contacts and deals, a nightly sync of employees into payroll, a script
that provisions a new site. It needs an account **Bearer** token (`apiKey`), never shipped to a
browser.

By the end you'll have a server client that reads private data, walks whole collections, writes
with proper error handling, and the CLI for the same work from a terminal.

### Create a server client with `apiKey`

One client, built with the account Bearer token and imported only from server code: a route
handler, a server action, a Server Component or a plain Node script.

```ts
// lib/uidu-admin.ts — never imported from a 'use client' file
import { createClient } from '@uidu/client';

export const uiduAdmin = createClient({
  workspace: process.env.UIDU_WORKSPACE!,
  apiKey: process.env.UIDU_API_KEY, // server-only — no NEXT_PUBLIC_ prefix
});
```

Where the token comes from, and why it never reaches the browser:
[Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

### Read private data

Contacts and deals, people and goals are readable only with the Bearer. A route handler that
feeds an internal dashboard:

```ts
// app/api/dashboard/route.ts
import {
  listContacts,
  listDeals,
  listEmployees,
  listGoals,
  goalProgress,
  isGoalBehind,
} from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

export async function GET() {
  const [contacts, deals, employees, goals] = await Promise.all([
    listContacts(uiduAdmin), // every contact — not paginated
    listDeals(uiduAdmin, { first: 50 }),
    listEmployees(uiduAdmin, { first: 50 }),
    listGoals(uiduAdmin),
  ]);

  const behind = goals
    .filter((g) => !g.parentId) // objectives, not key results
    .filter((g) => isGoalBehind(g, { tolerance: 0.1 }))
    .map((g) => ({ name: g.name, done: goalProgress(g), status: g.status }));

  return Response.json({
    contacts: contacts.length,
    openDeals: deals.map((d) => ({ name: d.name, amount: d.amount, status: d.status?.name })),
    people: employees.map((e) => e.user?.name),
    behind,
  });
}
```

<Callout type="info" title="Empty is a symptom">
  A list that comes back empty (or a `get*` that returns `null`) usually means the client was built
  with a public token: private data isn't an error for it, just invisible. Check that the client
  has `apiKey` before debugging the query.
</Callout>

Fields, filters and helpers: [Contacts & deals](https://developers.uidu.org/docs/domains/contacts.md) (`amount` is in cents),
[People](https://developers.uidu.org/docs/domains/people.md), [Goals](https://developers.uidu.org/docs/domains/goals.md) (values stored ×100).

### Read everything

`list*` returns one page. For a sync job or an export, `paginate` walks every page of a list
with its generated `*Document`:

```ts
import { paginate, ListDealsDocument, ListEmployeesDocument } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

const deals = await paginate(uiduAdmin, ListDealsDocument, {
  variables: { first: 100 },
  extract: (data) => data.currentWorkspace?.deals,
});

const employees = await paginate(uiduAdmin, ListEmployeesDocument, {
  variables: { first: 100 },
  extract: (data) => data.currentWorkspace?.employees,
});
```

`listContacts` already returns every contact: the field isn't a connection, so there is nothing
to paginate. More in [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).

### Write, and handle `errors`

Every write takes `{ input: { attributes } }` (plus `id` to update) and resolves to a payload
with an `errors` array. A &#x2A;*non-empty `errors`** is a validation failure — nothing is thrown, and
the record is unchanged. A transport or GraphQL failure, instead, **throws** a `UiduError`.

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

export async function moveDeal(id: string, statusId: string) {
  try {
    const result = await updateDeal(uiduAdmin, {
      input: { id, attributes: { statusId } },
    });

    if (result?.errors?.length) {
      // validation: e.g. [{ key: 'status', name: '…', type: '…' }]
      return { ok: false, errors: result.errors.map((e) => e.name) };
    }
    return { ok: true, id: result?.deal?.id };
  } catch (err) {
    if (err instanceof UiduError && err.code === 'GRAPHQL_ERROR') {
      console.error(err.graphQLErrors);
      return { ok: false, errors: ['The server rejected the request.'] };
    }
    throw err; // NETWORK_ERROR and anything else: let it surface
  }
}
```

Where the record sits in the payload varies by mutation — `updateDeal` returns it at `deal`,
`createDeal` at `deal.node`, `createTask` at `task`. The domain page shows each one. The input
shape and the full capability matrix: [Writes](https://developers.uidu.org/docs/foundations/writes.md); error codes and what to
retry: [Errors](https://developers.uidu.org/docs/foundations/errors.md).

### Provision from the terminal

The `uidu` CLI runs the same functions with the same Bearer: sign in once, then
`<entity> list|get|create|update|delete`. With `--json`, stdout carries only JSON, so you can pipe
it.

```bash
uidu login --workspace acme          # browser sign-in, token stored in ~/.uidu/config.json
uidu whoami --json                   # check the workspace and that the token hasn't expired

# read
uidu deals list --first 50 --json
uidu employees list --json | jq -r '.[].user.email'

# write — exits non-zero and prints the errors when `errors` isn't empty
uidu tasks create --attributes '{"name":"Call back Acme","spaceId":"Z2lkOi8v…"}' --json
uidu deals update Z2lkOi8v… --attributes '{"statusId":"Z2lkOi8v…"}' --json
```

In CI, set `UIDU_WORKSPACE` and `UIDU_API_KEY` instead of running `login` (flags win over env,
env over `~/.uidu/config.json`). Every verb per entity: [CLI commands](https://developers.uidu.org/docs/tools/cli/commands.md);
sign-in flows and token expiry: [Login](https://developers.uidu.org/docs/tools/cli/login.md).

### Run actions the SDK doesn't cover yet

The platform exposes more actions than the SDK has typed functions for. Any of them runs today,
by key, through the generic dispatcher — `executeAction` in code, `uidu tools call` in the
terminal. `attributes` uses the entity's own camelCase fields, and associations are GlobalIDs (the
`id` a previous `list` or `get` returned).

```ts
import { executeAction } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';

const result = await executeAction(uiduAdmin, {
  input: {
    key: 'Budgets::Budget::Create',
    attributes: { fiscalYear: 2027, currency: 'EUR' },
  },
});

if (result?.errors?.length) {
  // same rule as every write: validation errors come back, they aren't thrown
} else {
  result?.result; // what the action returned, as JSON
}

// The same from the terminal:
//   uidu tools call Budgets::Budget::Create --attributes '{"fiscalYear":2027,"currency":"EUR"}' --json
```

For an action on an existing record, pass its `id` in `input` as well — `executeAction` only;
`uidu tools call` takes `--attributes` alone. Every available key, and which ones the SDK covers:
[Roadmap](https://developers.uidu.org/docs/reference/roadmap.md). Field names per entity: [Schema](https://developers.uidu.org/docs/reference/schema.md).

## Where to go next

* [Writes](https://developers.uidu.org/docs/foundations/writes.md) — the input shape, `errors`, and which verbs each entity supports.
* [Paginate every page](https://developers.uidu.org/docs/recipes/paginate.md) — worked examples of `paginate`.
* [Bookings](https://developers.uidu.org/docs/domains/bookings.md) and [Campaigns](https://developers.uidu.org/docs/domains/campaigns.md) — the other private-data domains.
* [Spaces, tasks & notes](https://developers.uidu.org/docs/domains/workspace.md) — `createTask`, `createNote`, `createSpace`.
* [The `uidu` CLI](https://developers.uidu.org/docs/tools/cli.md) — everything the terminal can do, including scaffolding.
* [Client reference](https://developers.uidu.org/docs/reference/client.md) — every function, with its auth.

---

# Demo app

URL: https://developers.uidu.org/docs/recipes/demo-app

> A complete Next.js 16 example exercising every domain in @uidu/client.

The repo ships with a complete Next.js 16 demo at [`apps/demo/`](https://github.com/uidu-org/api.js/tree/main/apps/demo).

**Try it live: [api-js-demo.vercel.app](https://api-js-demo.vercel.app)** — it reads from the seeded `demo` workspace on staging (`demo.uidu.dev`).

## What it covers

| Route                        | What it shows                                                |
| ---------------------------- | ------------------------------------------------------------ |
| `/`                          | CMS landing page rendered block by block with `<PageBlocks>` |
| `/events`, `/events/[id]`    | Event listing and detail, with RSVP                          |
| `/stories`, `/stories/[id]`  | Stories (blog) listing and detail                            |
| `/careers`, `/careers/[id]`  | Job openings and the application flow                        |
| `/forms`, `/forms/[id]`      | Any uidu form rendered with `<DynamicForm>`                  |
| `/help`, `/help/[articleId]` | Help center channel, collections and articles                |
| `/donate`, `/donate/[id]`    | Donation campaigns                                           |
| `/bookings`, `/courses`      | Calendars and bookings, courses and lectures                 |
| `/store`, `/memberships`     | Storefront and membership UI patterns                        |
| `/custom-app`                | A custom app built on the same client                        |
| `/admin/*`                   | Employees, schedule, timesheet, CRM, campaigns, tasks, sites |

Where a workspace has no data for a domain, a few pages fall back to sample data so the UI is still visible.

<Callout type="info">
  The live demo runs without an API key, so it is **read-only**: form
  submissions, RSVPs and the `/admin` pages need an API key, which the public
  deploy does not have. Run it locally with `UIDU_API_KEY` set to try them.
</Callout>

## Run it locally

```bash
# from the repo root
cp apps/demo/.env.example apps/demo/.env.local
# defaults point at the staging `demo` workspace
yarn install
yarn workspace demo dev
```

Open [http://localhost:3002](http://localhost:3002).

## Configuration

Everything is driven by env vars (see `apps/demo/.env.example`):

| Variable                             | Default    | Purpose                                                                          |
| ------------------------------------ | ---------- | -------------------------------------------------------------------------------- |
| `UIDU_HOST`                          | `uidu.dev` | Which uidu to talk to. Staging by default; set `uidu.org` to opt into production |
| `UIDU_WORKSPACE`                     | `demo`     | Workspace subdomain                                                              |
| `UIDU_PUBLIC_TOKEN`                  | —          | Optional public read token                                                       |
| `UIDU_API_KEY`                       | —          | Server-only Bearer token for mutations and `/admin`                              |
| `UIDU_LANDING_SLUG`                  | `landing`  | CMS page rendered on `/`                                                         |
| `UIDU_PROJECT_ID`, `UIDU_CHANNEL_ID` | resolved   | Pin a specific CMS project / help-center channel                                 |

The CMS project and the help-center channel are resolved from the workspace at runtime, so nothing workspace-specific is committed.

## How it's built

### `lib/uidu.ts` — one client

```ts
import { createClient } from '@uidu/client';

const host = process.env.UIDU_HOST ?? 'uidu.dev';
const workspace = process.env.UIDU_WORKSPACE ?? 'demo';

export const uidu = createClient({
  workspace,
  endpoint: `https://${workspace}.${host}/graphql`,
  publicToken: process.env.UIDU_PUBLIC_TOKEN,
  apiKey: process.env.UIDU_API_KEY,
});
```

One client, used from every Server Component and Server Action. No provider needed on the server.

### Landing page — CMS to JSX

```tsx
// app/page.tsx
import { getPage } from '@uidu/client';
import { PageBlocks } from '@uidu/react';
import { ContactForm } from '@/components/blocks/ContactForm';
import { Feature } from '@/components/blocks/Feature';
import { Hero } from '@/components/blocks/Hero';
import { config, defaultProjectId, uidu } from '@/lib/uidu';

export default async function HomePage() {
  const projectId = await defaultProjectId();
  const page = projectId
    ? await getPage(uidu, { projectId, slug: config.landingSlug })
    : null;
  if (!page) return <NotFound />;

  return (
    <PageBlocks
      pageBlocks={page.pageBlocks}
      components={{ Header: Hero, feature1: Feature, ModuloContatto: ContactForm }}
    />
  );
}
```

`<PageBlocks>` walks the blocks, matches each block's shortname against the components map, and renders it. Block components read their fields with `useFields(block.fieldValues)` and `toText()`.

### Contact form — Server Action mutation

The `ModuloContatto` block carries a linked `Form` record. The block hands it to a `'use client'` component that submits through a Server Action:

```ts
// lib/actions.ts
'use server';
import { createFormResponse } from '@uidu/client';
import { uidu } from './uidu';

export async function submitContactForm(formId, fieldValues, contact) {
  const result = await createFormResponse(uidu, {
    input: {
      attributes: { formId, fieldValuesAttributes: /* … */, contactAttributes: /* … */ },
      event: 'complete!',
    },
  });
  return result?.errors?.length
    ? { ok: false, errors: result.errors }
    : { ok: true, responseId: result.formResponse.id };
}
```

### Help center — one query, full tree

```tsx
// app/help/page.tsx
const channelId = await defaultChannelId();
const channel = channelId ? await getChannel(uidu, { id: channelId }) : null;
```

One `getChannel` call returns the channel with its collections and articles; the page renders the tree without further fetches.

## Files at a glance

```
apps/demo/src/
├── app/
│   ├── page.tsx              # Landing — getPage() + <PageBlocks>
│   ├── events/ stories/ careers/ forms/ help/ donate/ …
│   └── admin/                # API-key pages
├── components/
│   ├── Nav.tsx
│   ├── blocks/               # Hero, Feature, ContactForm (+ client part)
│   ├── themes/               # per-section layout variants
│   └── ui/                   # shadcn primitives
└── lib/
    ├── uidu.ts               # createClient() + project/channel resolution
    └── actions.ts            # Server Actions (forms, RSVP, applications, …)
```

## Patterns to take away

1. **One client instance per app.** Server-side, no providers. Pass it to every domain function.
2. **CMS blocks → component map.** The CMS holds the shape, your code holds the components.
3. **Server Components for fetching, Server Actions for mutating, Client Components for interactivity.**
4. **Linked records.** When a block embeds a record (a Form inside a page block), the typed response exposes it inline — no second round trip.
5. **Keep secrets server-side.** `UIDU_API_KEY` is only read in Server Components and Server Actions, never shipped to the browser.

---

# Dynamic CMS routes

URL: https://developers.uidu.org/docs/recipes/dynamic-cms-routes

> Generate a static page for every CMS slug at build time with generateStaticParams.

<Callout type="warn" title="Legacy CMS">
  This recipe uses the legacy Projects CMS (`listPages` / `getPage`). For a Site, build the params from
  `listSitePages` — see step 6 of the [Public website](https://developers.uidu.org/docs/paths/public-website.md) path.
</Callout>

A `[slug]/page.tsx` route that pre-renders every CMS page in your project.

## The route

```tsx
// app/[slug]/page.tsx
import { getPage, listPages } from '@uidu/client';
import { PageBlocks } from '@uidu/react';
import { notFound } from 'next/navigation';
import { client } from '@/lib/uidu';
import { Hero, Feature, ContactForm } from '@/components/blocks';

export async function generateStaticParams() {
  const pages = await listPages(client, {
    projectId: process.env.UIDU_PROJECT_ID!,
    first: 200,
  });
  return pages.map((p) => ({ slug: p.slug }));
}

export default async function CmsPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const page = await getPage(client, {
    projectId: process.env.UIDU_PROJECT_ID!,
    slug,
  });

  if (!page) notFound();

  return (
    <PageBlocks
      pageBlocks={page.pageBlocks}
      components={{
        Hero,
        feature1: Feature,
        ContactForm,
      }}
    />
  );
}
```

## Why this works

* `listPages` returns every page slug in the project — that's what `generateStaticParams` needs.
* `getPage` is called once per slug at build time. Next.js caches the result per route.
* `<PageBlocks>` looks up each block's `templateBlock.shortname` in your `components` map and renders the matching component with the block's `fieldValues`.

## Add a sitemap

```tsx
// app/sitemap.ts
import { listPages } from '@uidu/client';
import { client } from '@/lib/uidu';

export default async function sitemap() {
  const pages = await listPages(client, {
    projectId: process.env.UIDU_PROJECT_ID!,
    first: 500,
  });

  return pages.map((p) => ({
    url: `https://example.com/${p.slug}`,
    lastModified: p.updatedAt,
  }));
}
```

## Add ISR revalidation

If you want the page to refresh without a redeploy, set a revalidation interval:

```tsx
export const revalidate = 60; // seconds
```

For event-driven invalidation, configure a webhook in your uidu workspace to hit `/api/revalidate?path=/<slug>` and call `revalidatePath` from there.

<Callout type="info">
  For more than \~500 pages, walk every page with the [`paginate()`](https://developers.uidu.org/docs/recipes/paginate.md) helper.
</Callout>

---

# File upload through Forms

URL: https://developers.uidu.org/docs/recipes/file-upload

> Use the uploadPresignedUrl flow to attach files to a form submission.

`<DynamicForm>` has no file input — an `attachments` question renders as a plain text input. To
collect files, render that question yourself with the presigned-upload flow below.

## How it works

1. **Read the upload target from the form.** `getForm` returns it on each media question's field:
   `field.uploadPresignedUrl` is the presign as JSON — `{ url, fields }`, a presigned **POST** — and
   `field.uploadUrl` / `field.uploadParams` are its two halves. This is a public read.
2. **POST the file** from the browser straight to `uploadUrl`, as multipart form data: every entry of
   `uploadParams`, then the file. No uidu token is involved.
3. **Submit the response** from a server action, with the uploaded file as a linked `Attachment` on
   that question's field value.

<Callout type="warn" title="Read the form close to the upload">
  Every read of the form signs a fresh target, and it expires after a short while. Only media fields
  have one; other fields return it empty.
</Callout>

## The page

```tsx
// app/apply/page.tsx — a Server Component
import { getForm } from '@uidu/client';
import { notFound } from 'next/navigation';
import { uidu } from '@/lib/uidu';
import { ApplyForm } from './apply-form';

const FORM_ID = 'Z2lkOi8v…';

export default async function ApplyPage() {
  const form = await getForm(uidu, { id: FORM_ID });
  if (!form) notFound();

  const fileQuestion = form.inputs.edges
    ?.map((e) => e?.node)
    .find((q) => q?.field.kind === 'attachments');
  if (!fileQuestion) notFound();

  return (
    <ApplyForm
      formId={form.id}
      fieldId={fileQuestion.field.id}
      uploadUrl={fileQuestion.field.uploadUrl as string}
      uploadParams={(fileQuestion.field.uploadParams ?? {}) as Record<string, string>}
    />
  );
}
```

## The upload (client)

```tsx
// app/apply/apply-form.tsx
'use client';

import { useState } from 'react';
import { submitWithFile } from './actions';

export function ApplyForm(props: {
  formId: string;
  fieldId: string;
  uploadUrl: string;
  uploadParams: Record<string, string>;
}) {
  const [file, setFile] = useState<string | null>(null);
  const [status, setStatus] = useState<'idle' | 'uploading' | 'uploaded' | 'sent'>('idle');

  async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    const picked = e.target.files?.[0];
    if (!picked) return;
    setStatus('uploading');

    const body = new FormData();
    Object.entries(props.uploadParams).forEach(([key, value]) => body.append(key, value));
    body.append('file', picked); // the file goes last, after the signed fields

    const res = await fetch(props.uploadUrl, { method: 'POST', body });
    if (!res.ok) throw new Error('upload failed');

    // uidu's attachment data for the uploaded file: the object key without its `cache/` prefix.
    setFile(
      JSON.stringify({
        id: props.uploadParams.key.replace(/^cache\//, ''),
        storage: 'cache',
        metadata: { filename: picked.name, mime_type: picked.type, size: picked.size },
      }),
    );
    setStatus('uploaded');
  }

  async function send() {
    if (!file) return;
    const result = await submitWithFile(props.formId, props.fieldId, file);
    if (result.ok) setStatus('sent');
  }

  return (
    <div>
      <input type="file" onChange={handleChange} />
      <button disabled={status !== 'uploaded'} onClick={send}>
        Send
      </button>
      <span>{status}</span>
    </div>
  );
}
```

## Submitting (server action)

The response is a write, so it runs on the server with the API key client:

```ts
// app/apply/actions.ts
'use server';

import { createFormResponse } from '@uidu/client';
import { uidu } from '@/lib/uidu'; // createClient({ workspace, publicToken, apiKey })

export async function submitWithFile(formId: string, fieldId: string, file: string) {
  const result = await createFormResponse(uidu, {
    input: {
      attributes: {
        formId,
        fieldValuesAttributes: [
          {
            fieldId,
            linkedRecordType: 'Attachment',
            linkedRecordAttributes: { file }, // the attachment data from the upload
          },
          // other answers as usual: { fieldId, content: { value } }
        ],
      },
      event: 'complete!',
    },
  });

  if (result?.errors?.length) return { ok: false as const, errors: result.errors };
  return { ok: true as const, responseId: result?.formResponse?.id };
}
```

* `'Attachment'` is the only `linkedRecordType` the API accepts on a field value; the file lives in
  that linked record, not in `content`.
* Every other answer in the same response still uses `content: { value }` — see
  [Forms](https://developers.uidu.org/docs/domains/forms.md).
* The same flow serves a call's application form: its questions are at
  `call.form.inputs.edges[].node` — see [Calls](https://developers.uidu.org/docs/domains/calls.md).

More on the response shape, drafts and statuses: [Forms](https://developers.uidu.org/docs/domains/forms.md).

---

# Multi-step forms

URL: https://developers.uidu.org/docs/recipes/multi-step-forms

> Build a multi-step submission flow with createFormResponse + updateFormResponse, saving a draft at every step.

Long forms break into steps. Don't gate the visitor on a single final submit: save each step as a
**draft** response, and complete it on the last one.

## The pattern

1. **First step:** call `createFormResponse` with the first answers and &#x2A;*no `event`**. The response
   is saved as a draft (status `initialized`). Keep the returned `id`.
2. **Next steps:** call `updateFormResponse` with `{ id, attributes }` and the next answers. An
   answer you already saved is changed by passing its &#x2A;*field value `id`**; one without an `id` is
   added.
3. **Last step:** the same update with `event: 'complete!'`. The response becomes `completed`, and
   that is what announces it to the workspace. `'complete!'` is the only event, and it fires once.

Both mutations are writes, so they need the client with the API key and run in **server actions**,
never in the browser. See [Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## The server actions

```ts
// app/apply/actions.ts
'use server';

import { createFormResponse, updateFormResponse } from '@uidu/client';
import { uidu } from '@/lib/uidu'; // createClient({ workspace, publicToken, apiKey })

export type Answer = { fieldId: string; value: unknown };

export type StepResult =
  | { ok: true; responseId: string; fieldValueIds: Record<string, string> }
  | { ok: false; errors: Array<{ key?: string | null; name?: string | null; type?: string | null }> };

// field id → field value id, so the next save updates an answer instead of adding a second one.
function idsByField(
  fieldValues: Array<{ id: string; field: { id: string } }> | null | undefined,
): Record<string, string> {
  return Object.fromEntries((fieldValues ?? []).map((fv) => [fv.field.id, fv.id]));
}

export async function saveStep(
  formId: string,
  draft: { responseId: string; fieldValueIds: Record<string, string> } | null,
  answers: Answer[],
  last: boolean,
): Promise<StepResult> {
  const event = last ? 'complete!' : undefined; // no event = stays a draft

  const fieldValuesAttributes = answers.map((a) => ({
    id: draft?.fieldValueIds[a.fieldId], // set → update the saved answer; undefined → add one
    fieldId: a.fieldId,
    content: { value: a.value }, // always { value }, for every field kind
  }));

  const result = draft
    ? await updateFormResponse(uidu, {
        input: { id: draft.responseId, event, attributes: { formId, fieldValuesAttributes } },
      })
    : await createFormResponse(uidu, {
        input: { event, attributes: { formId, fieldValuesAttributes } },
      });

  if (result?.errors?.length) return { ok: false, errors: result.errors };
  const response = result?.formResponse;
  if (!response?.id) return { ok: false, errors: [{ name: 'No response returned' }] };

  return { ok: true, responseId: response.id, fieldValueIds: idsByField(response.fieldValues) };
}
```

* `formId` goes **inside** `attributes`, next to `fieldValuesAttributes`; `id` and `event` sit beside
  `attributes` on the input.
* Validation failures come back in `errors`, not as a throw — the draft is unchanged when they do.
  See [Writes](https://developers.uidu.org/docs/foundations/writes.md).
* Every payload returns `formResponse.fieldValues[]` with each value's `id` and `field.id`, and
  `formResponse.status` (`initialized` while it's a draft, `completed` after `'complete!'`).

## The client component

The browser only holds the draft's ids and calls the actions; the questions come from `getForm`
(`form.inputs`), read with the public token in the page that renders this.

```tsx
'use client';

import { useState } from 'react';
import { saveStep, type Answer } from './actions';

type Draft = { responseId: string; fieldValueIds: Record<string, string> };

export function MultiStepForm({ formId, steps }: { formId: string; steps: number }) {
  const [draft, setDraft] = useState<Draft | null>(null);
  const [step, setStep] = useState(1);
  const [done, setDone] = useState(false);

  async function submitStep(answers: Answer[]) {
    const last = step === steps;
    const result = await saveStep(formId, draft, answers, last);
    if (!result.ok) {
      // show result.errors next to the fields
      return;
    }
    setDraft({ responseId: result.responseId, fieldValueIds: result.fieldValueIds });
    if (last) setDone(true);
    else setStep(step + 1);
  }

  if (done) return <p>Thanks — your answers are in.</p>;
  return <Step n={step} onNext={submitStep} />; // your per-step fields
}
```

Split the questions into steps however suits the form — by `position`, or by groups you define —
and pass each step's answers as `{ fieldId: question.field.id, value }`.

## Resuming a session

To let a visitor come back later, keep the draft (`responseId` and the `fieldValueIds` map) in a
cookie keyed by `formId`, set from the server action, and start the component from it. `@uidu/client`
has no function that reads a form response back, so keep the answers you want to prefill on your side
too (the same cookie, or your own storage).

## Validation per step

Validate each step on the client before calling `saveStep` — e.g. `react-hook-form` with
`mode: 'onBlur'` and `handleSubmit(onValid)` — so the visitor doesn't wait for a round trip on every
field. The server is still the source of truth: show the `errors` it returns.

The shape of every answer and the response lifecycle: [Forms](https://developers.uidu.org/docs/domains/forms.md). The `{ value }`
rule in full: [Pagination & field values](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md).

---

# Paginate every page

URL: https://developers.uidu.org/docs/recipes/paginate

> Walk every page of a Relay connection with the paginate() helper.

Most `list*` functions in `@uidu/client` return the first page only — they're tuned for the common case of "show the first N." When you need *everything* (a sitemap, `generateStaticParams`, an export), use the `paginate()` helper.

## What it does

`paginate()` repeatedly calls a list query, threading `pageInfo.endCursor` into the next call's `after` argument until `hasNextPage` is `false`. Returns a flat array of nodes.

## Signature

```ts
import { paginate, ListEventsDocument } from '@uidu/client';

const all = await paginate(client, ListEventsDocument, {
  variables: { first: 100 },
  extract: (d) => d.currentWorkspace?.events,
});
```

| Argument    | Type                                | Notes                                                      |
| ----------- | ----------------------------------- | ---------------------------------------------------------- |
| `client`    | `UiduClient`                        | Created with `createClient`.                               |
| `document`  | `TypedDocumentNode`                 | Any `List*Document` re-exported from `@uidu/client`.       |
| `variables` | `Omit<Vars, 'after'>`               | First-page variables. `after` is set by `paginate` itself. |
| `extract`   | `(data) => RelayConnection \| null` | Pluck the connection out of the result shape.              |
| `maxPages`  | `number` (default 1000)             | Safety bound — guards against runaway loops.               |

## Sitemap with every page

```tsx
// app/sitemap.ts
import { paginate, ListPagesDocument } from '@uidu/client';
import { client } from '@/lib/uidu';

export default async function sitemap() {
  const pages = await paginate(client, ListPagesDocument, {
    variables: { projectId: process.env.UIDU_PROJECT_ID!, first: 100 },
    extract: (d) => d.currentWorkspace?.project?.pages,
  });

  return pages.map((p) => ({
    url: `https://example.com/${p.slug}`,
    lastModified: p.updatedAt,
  }));
}
```

## generateStaticParams for every event

```tsx
// app/events/[id]/page.tsx
import { paginate, ListEventsDocument } from '@uidu/client';
import { client } from '@/lib/uidu';

export async function generateStaticParams() {
  const events = await paginate(client, ListEventsDocument, {
    variables: { first: 100 },
    extract: (d) => d.currentWorkspace?.events,
  });
  return events.map((e) => ({ id: e.id }));
}
```

## Other List Documents

The same pattern works with any of the re-exported Documents:

`ListBookingsDocument`, `ListCalendarsDocument`, `ListCalendarEventsDocument`, `ListCallsDocument`, `ListCampaignsDocument`, `ListEmailCampaignsDocument`, `ListCoursesDocument`, `ListDealsDocument`, `ListDonationCampaignsDocument`, `ListEmployeesDocument`, `ListEnrollmentsDocument`, `ListEventsDocument`, `ListFormsDocument`, `ListKbArticlesDocument`, `ListKbCollectionsDocument`, `ListLecturesDocument`, `ListPagesDocument`, `ListStoriesDocument`.

## Why a Document, not the list function?

The user-facing `listEvents(client, vars)` returns a flat array — it throws away `pageInfo` to keep the common case ergonomic. `paginate()` needs `pageInfo`, so it works one level lower against the raw `TypedDocumentNode`.

<Callout type="warn">
  Be mindful with very large collections. `paginate()` is memory-bound — it holds the full result in RAM. For exports of 10k+ records, prefer streaming the data straight to disk or a downstream sink instead.
</Callout>

---

# Search-as-you-type

URL: https://developers.uidu.org/docs/recipes/search-as-you-type

> Debounced live search across events and stories, through a route handler that runs search on the server.

A search box that updates as the visitor types. The input is a Client Component, and it calls a
route handler that runs [`search`](https://developers.uidu.org/docs/domains/search.md) on the server. `search` returns references
(`__typename` + `id`), so the route also fetches what each result shows. Keeping all of it on the
server means the browser never holds a token, and the follow-up requests happen in one place.

## The route handler

```ts
// src/app/api/search/route.ts
import { getEvent, getStory, search } from '@uidu/client';
import { uidu } from '@/lib/uidu'; // server-only client

export type SearchHit = { id: string; type: 'Event' | 'Story'; title: string; href: string };

export async function GET(request: Request) {
  const q = new URL(request.url).searchParams.get('q')?.trim() ?? '';
  if (q.length < 2) return Response.json({ hits: [], total: 0 });

  const result = await search(uidu, {
    models: ['Event', 'Story'], // always pass models
    params: { term: q },
    first: 8,
  });
  const nodes = (result?.edges ?? []).flatMap((e) => (e?.node ? [e.node] : []));

  // Each node is only { __typename, id }: fetch the title from its own domain.
  const hits = await Promise.all(
    nodes.map(async (node): Promise<SearchHit | null> => {
      const href = (base: string) => `${base}/${encodeURIComponent(node.id)}`;
      switch (node.__typename) {
        case 'Event': {
          const event = await getEvent(uidu, { id: node.id });
          return event && { id: node.id, type: 'Event', title: event.name ?? '', href: href('/event') };
        }
        case 'Story': {
          const story = await getStory(uidu, { id: node.id });
          return story && { id: node.id, type: 'Story', title: story.name ?? '', href: href('/stories') };
        }
        default:
          return null;
      }
    }),
  );

  return Response.json({
    hits: hits.filter((h): h is SearchHit => h !== null),
    total: result?.totalCount ?? 0,
  });
}
```

* `models` restricts the search to the record types you can render. Pick yours from the `node`
  union of `SearchResult`. Knowledge-base articles aren't in the index; to search a help center,
  filter its articles in memory ([Help Center](https://developers.uidu.org/docs/domains/help-center.md)).
* Keep `first` small: every hit costs one more `get*` request.
* An empty result can also mean the index is unavailable: `search` returns no hits instead of
  throwing. The operators, sorting and pagination are covered on [Search](https://developers.uidu.org/docs/domains/search.md).

## The component

```tsx
// src/components/live-search.tsx
'use client';

import { useEffect, useState } from 'react';
import Link from 'next/link';
import type { SearchHit } from '@/app/api/search/route';

export function LiveSearch() {
  const [query, setQuery] = useState('');
  const [hits, setHits] = useState<SearchHit[]>([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (query.trim().length < 2) {
      setHits([]);
      return;
    }

    const controller = new AbortController();
    const timer = setTimeout(async () => {
      setLoading(true);
      try {
        const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
          signal: controller.signal,
        });
        const data: { hits: SearchHit[] } = await res.json();
        setHits(data.hits);
      } catch (err) {
        if ((err as Error).name !== 'AbortError') throw err;
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    }, 200);

    return () => {
      clearTimeout(timer);
      controller.abort(); // a newer keystroke wins: drop the stale request
    };
  }, [query]);

  return (
    <div>
      <input
        type="search"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search…"
        aria-label="Search"
      />
      {loading && <span>Searching…</span>}
      <ul>
        {hits.map((hit) => (
          <li key={hit.id}>
            <Link href={hit.href}>{hit.title}</Link>
            <small> · {hit.type}</small>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

## Why 200ms

It's shorter than the gap between keystrokes for most people, and long enough to skip requests
fired mid-word. If your visitors are far from the server, raise it to 300ms.

## Cancel stale requests

If the visitor types fast, results for earlier characters can arrive *after* later ones. The effect
above aborts the previous `fetch` whenever the query changes. The `AbortController` belongs to
`fetch` in the browser: `search` itself takes no signal, and the server finishes the search it
started.

## Group by record type

```tsx
const grouped = hits.reduce<Record<string, SearchHit[]>>((acc, hit) => {
  (acc[hit.type] ??= []).push(hit);
  return acc;
}, {});

return (
  <div>
    {Object.entries(grouped).map(([type, items]) => (
      <section key={type}>
        <h3>{type}</h3>
        <ul>
          {items.map((hit) => (
            <li key={hit.id}>
              <Link href={hit.href}>{hit.title}</Link>
            </li>
          ))}
        </ul>
      </section>
    ))}
  </div>
);
```

The server already returns hits in relevance order, and grouping keeps that order within each type.

---

# @uidu/app-bridge reference

URL: https://developers.uidu.org/docs/reference/app-bridge

> Every export of @uidu/app-bridge, generated from the source.

The browser half of a custom app that runs inside uidu. In React, prefer `<UiduAppProvider>` from `@uidu/react`, which wraps it. Guide: [Custom app](https://developers.uidu.org/docs/paths/custom-app.md).

## Functions

| Export                                                                                                                                                                                | Description                                                                                                                                                                                                                                                                                   | Demo |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| [`connect`](https://github.com/uidu-org/api.js/blob/main/packages/app-bridge/src/bridge.ts)<br />`connect(options: ConnectOptions = {}): Promise<AppBridge>`                          | Ask the uidu page framing this app for a session, and resolve once it has one. Rejects with an `AppBridgeError` when the app isn't framed by a trusted uidu page, or the host refuses or doesn't answer — so an app can fall back (to local storage, a login screen…).                        | —    |
| [`detectParentOrigin`](https://github.com/uidu-org/api.js/blob/main/packages/app-bridge/src/origins.ts)<br />`detectParentOrigin(win: Window): string \| null`                        | The origin of the page that framed this one, when the browser says: `location.ancestorOrigins` (Chromium, Safari) first, since it can't be suppressed by a referrer policy; `document.referrer` otherwise (Firefox). uidu frames apps with `strict-origin-when-cross-origin`, which sends it. | —    |
| [`isTrustedOrigin`](https://github.com/uidu-org/api.js/blob/main/packages/app-bridge/src/origins.ts)<br />`isTrustedOrigin(origin: string, patterns: ReadonlyArray<string>): boolean` | Whether `origin` matches any of `patterns`, each an exact origin or a `https://*.example.org` wildcard (see `matchesOrigin`).                                                                                                                                                                 | —    |
| [`matchesOrigin`](https://github.com/uidu-org/api.js/blob/main/packages/app-bridge/src/origins.ts)<br />`matchesOrigin(origin: string, pattern: string): boolean`                     | `pattern` is an exact origin (`https://me.uidu.local:8443`) or a wildcard one (`https://*.uidu.org`), where `*` stands for one or more subdomain labels — never for the bare domain, the scheme or the port.                                                                                  | —    |
| [`parseHostMessage`](https://github.com/uidu-org/api.js/blob/main/packages/app-bridge/src/protocol.ts)<br />`parseHostMessage(data: unknown): HostMessage \| null`                    | Narrow an inbound `event.data` to a host message, or null. Checks only the shape — whether the sender is trusted is the caller's job.                                                                                                                                                         | —    |

## Classes

| Export                                                                                             | Description                                                                                                                               | Demo |
| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| [`AppBridgeError`](https://github.com/uidu-org/api.js/blob/main/packages/app-bridge/src/bridge.ts) | Why the bridge failed or gave up, by `code` (`NOT_EMBEDDED`, `UNTRUSTED_HOST`, `TIMEOUT`, `HOST_ERROR`, …); `connect()` rejects with one. | —    |

## Constants

| Export                                                                                                    | Description                                                                                                                                                                                                    | Demo                                                                                                            |
| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [`DEFAULT_HOST_ORIGINS`](https://github.com/uidu-org/api.js/blob/main/packages/app-bridge/src/origins.ts) | Which pages an app lets hand it a session.                                                                                                                                                                     | [`app/custom-app/page.tsx`](https://github.com/uidu-org/api.js/blob/main/apps/demo/src/app/custom-app/page.tsx) |
| [`PROTOCOL_VERSION`](https://github.com/uidu-org/api.js/blob/main/packages/app-bridge/src/protocol.ts)    | The wire format between a custom app and the uidu page that frames it — protocol v1, as served by `CustomAppFrame.tsx` in the uidu repo (docs/superpowers/specs/2026-09-23-custom-space-apps/SPEC.md, step 2). | —                                                                                                               |

## Types

`AppBridge`, `AppBridgeErrorCode`, `AppContext`, `AppMessage`, `ConnectOptions`, `ContextMessage`, `ErrorMessage`, `HostMessage`, `Named`, `ReadyMessage`, `RefreshMessage`

---

# @uidu/client reference

URL: https://developers.uidu.org/docs/reference/client

> Every export of @uidu/client, grouped by domain — 154 functions, generated from the source.

Every function takes a `UiduClient` as its first argument. **Kind**: *read* runs a query, *write* runs a mutation and returns its payload (check `errors`), *helper* is pure. **Auth**: *public token* works in the browser; *Bearer* needs an account token (`apiKey`) and must run server-side — see [Writes](https://developers.uidu.org/docs/foundations/writes.md). **CLI** lists the `uidu` commands that call the function.

Domains: [Client & utilities](#core) · [CMS — Sites](#sites) · [CMS — Projects (legacy)](#cms) · [Stories](#stories) · [Help center](#help-center) · [Knowledge base](#kb) · [Search](#search) · [Forms](#forms) · [Events](#events) · [Donations](#donations) · [Courses](#courses) · [Calls](#calls) · [Jobs & applications](#jobs) · [Contacts & deals](#contacts) · [Campaigns](#campaigns) · [People (HR)](#people) · [Goals (OKRs)](#goals) · [Spaces, tasks & notes](#workspace) · [Bookings & calendars](#bookings) · [Models (custom data)](#models) · [Provisioning & actions](#provisioning)

## Client & utilities

Client construction, errors, pagination, field-value helpers.

Guide: [Client & utilities](https://developers.uidu.org/docs/foundations/data-model.md).

* [`UiduClient`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/client.ts) — Sends GraphQL documents to one uidu endpoint with the configured credentials, and throws a `UiduError` when a request fails.
* [`UiduError`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/types.ts) — What `UiduClient` throws: `code` is `GRAPHQL_ERROR` (with `graphQLErrors`), `NETWORK_ERROR`, `TOKEN_ERROR` or `CONFIG_ERROR`.

| Function                                                                                                                  | Kind   | Auth | CLI | Description                                                                                                                                       |
| ------------------------------------------------------------------------------------------------------------------------- | ------ | ---- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createClient`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/client.ts)                              | helper | —    | —   | Build a `UiduClient`; it needs a `workspace` or an `endpoint`, and authenticates with `publicToken`, `apiKey` or `getToken`.                      |
| [`fromBridge`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/client.ts)                                | helper | —    | —   | Client config for a custom app, from a connected `@uidu/app-bridge`: the bridge's GraphQL endpoint, with its session token asked for per request. |
| [`normalizeFieldValueContent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/fieldValues.ts)     | helper | —    | —   | Wrap a `content` payload in `{ value }` unless it already carries a `value` key. Returns the input untouched when there is nothing to do.         |
| [`normalizeFieldValuesAttributes`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/fieldValues.ts) | helper | —    | —   | Normalize a standalone `fieldValuesAttributes` array. Exported for callers that assemble one by hand outside an SDK mutation.                     |
| [`paginate`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/paginate.ts)                          | read   | —    | —   | Walks every page of a Relay-style connection and returns a flat array of nodes.                                                                   |

**Types:** `PaginateOptions`, `UiduClientConfig`, `UiduErrorOptions`

## CMS — Sites

The new CMS: Site → page/block Models → ModelItems.

Guide: [CMS — Sites](https://developers.uidu.org/docs/domains/cms.md).

| Function                                                                                         | Kind  | Auth         | CLI            | Description                                                                                                                                                                                             |
| ------------------------------------------------------------------------------------------------ | ----- | ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createSite`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)        | write | Bearer       | `sites create` | Provisions a new Site in the workspace/account (`tenantId`). Bearer token (`apiKey`) — this is workspace admin territory, never a public write. Returns the mutation payload as-is, including `errors`. |
| [`getPageBySlug`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)     | read  | public token | —              | A page of the Site by slug, with its field values and its ordered blocks. Only published pages unless `includeDrafts`.                                                                                  |
| [`getSingletonBlock`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts) | read  | public token | —              | A singleton of the Site — the header, the footer, the nav — by its Model's shortname, with its field values and blocks.                                                                                 |
| [`getSite`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)           | read  | public token | —              | A Site by its id — for previews, where there is no domain to go by.                                                                                                                                     |
| [`getSiteByDomain`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)   | read  | public token | —              | The Site served on `domain` (e.g. `www.acme.org`), or null.                                                                                                                                             |
| [`listSitePages`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/sites.ts)     | read  | public token | —              | The Site's pages, for navigation and sitemaps: published only unless `includeDrafts`, singletons (header, footer) left out.                                                                             |

**Types:** `Site`, `SiteBlock`, `SiteFields`, `SiteModel`, `SitePage`, `SitePageSummary`

## CMS — Projects (legacy)

Legacy CMS: Project → Page → PageBlock → FieldValue.

Guide: [CMS — Projects (legacy)](https://developers.uidu.org/docs/domains/cms.md).

| Function                                                                                      | Kind  | Auth         | CLI              | Description                                                                                                                                                                   |
| --------------------------------------------------------------------------------------------- | ----- | ------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createFieldValue`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts) | write | Bearer       | `field create`   | Create a field value, e.g. set a page block's field (`fieldId`, `content`). Returns the payload (with `errors`) or null; the new value is at `fieldValue.node`.               |
| [`createPage`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)       | write | Bearer       | `page create`    | Create a legacy CMS page from a `templateId`, with `slug` and optional `metadataAttributes` (there is no `projectId` attribute). Returns the payload (with `errors`) or null. |
| [`createPageBlock`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)  | write | Bearer       | `block create`   | Place a template block on a page (`pageId`, `templateBlockId`, both numeric). Returns the payload (with `errors`) or null.                                                    |
| [`createProject`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)    | write | Bearer       | `project create` | Create a legacy CMS project (`name`, `slug`, `domain`). Returns the payload (with `errors`) or null; the new project is at `project.node`.                                    |
| [`getPage`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)          | read  | public token | `page get`       | A legacy CMS page by project and `slug`, with its metadata and ordered `pageBlocks` (and any linked form's questions), or null.                                               |
| [`getTemplate`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)      | read  | public token | —                | A legacy CMS template by id, with every page it holds and their blocks, or null.                                                                                              |
| [`listPages`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)        | read  | public token | `pages list`     | The pages of a legacy CMS project (`id` and `slug`), one Relay page at a time; use `paginate` with `ListPagesDocument` for all of them.                                       |
| [`listProjects`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/cms.ts)     | read  | public token | `projects list`  | The workspace's CMS projects. A page lives in one, and `getPage` needs its id.                                                                                                |

**Types:** `Page`, `Template`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListPagesDocument`

## Stories

Blog posts / news.

Guide: [Stories](https://developers.uidu.org/docs/domains/stories.md).

| Function                                                                                       | Kind  | Auth         | CLI              | Description                                                                                                                  |
| ---------------------------------------------------------------------------------------------- | ----- | ------------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| [`createStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)   | write | Bearer       | `stories create` | Create a story; returns the payload as-is, `{ errors, story }`, validation failures included.                                |
| [`getStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)      | read  | public token | `stories get`    | One story with its rich-text `body`, or null.                                                                                |
| [`listStories`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/stories.ts)   | read  | public token | `stories list`   | The workspace's stories as a flat array of summaries (no `body`); page with `first`/`after`.                                 |
| [`updateStory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `stories update` | Update a story: `{ input: { id, attributes } }` (create it with `createStory`). Returns the payload (with `errors`) or null. |

**Types:** `Story`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListStoriesDocument`

## Help center

Help-center channels.

Guide: [Help center](https://developers.uidu.org/docs/domains/help-center.md).

| Function                                                                                          | Kind  | Auth         | CLI              | Description                                                                                                                                                                                |
| ------------------------------------------------------------------------------------------------- | ----- | ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`createChannel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `channel create` | Create a help-center channel. Returns the payload (with `errors`) or null. The new channel is at `channel.node`.                                                                           |
| [`deleteChannel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `channel delete` | Delete a help-center channel by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                          |
| [`getChannel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/help-center.ts)   | read  | public token | `channel get`    | One help center channel with its collections and their articles in a single request (the last 100 of each), or null.                                                                       |
| [`listChannels`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/help-center.ts) | read  | public token | `channel list`   | The workspace's help center channels. `isDefault` marks the one the workspace treats as its main channel — a caller that has no channel id configured should prefer it over the first row. |
| [`updateChannel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `channel update` | Update a help-center channel: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                |

**Types:** `Channel`

## Knowledge base

KB collections and articles.

Guide: [Knowledge base](https://developers.uidu.org/docs/domains/kb.md).

| Function                                                                                              | Kind  | Auth         | CLI                     | Description                                                                                                                                          |
| ----------------------------------------------------------------------------------------------------- | ----- | ------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createKbArticle`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer       | `kb-articles create`    | Create a knowledge-base article in `attributes.kbCollectionId`. Returns the payload (with `errors`) or null. The new article is at `kbArticle.node`. |
| [`createKbCollection`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `kb-collections create` | Create a knowledge-base collection. Returns the payload (with `errors`) or null. The new collection is at `kbCollection.node`.                       |
| [`deleteKbArticle`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer       | `kb-articles delete`    | Delete a knowledge-base article by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                 |
| [`deleteKbCollection`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `kb-collections delete` | Delete a knowledge-base collection by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                              |
| [`getKbArticle`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/kb.ts)              | read  | public token | `kb-articles get`       | One knowledge-base article with its rich-text `body` and its collection, or null.                                                                    |
| [`getKbCollection`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/kb.ts)           | read  | public token | `kb-collections get`    | One knowledge-base collection with its first 50 articles, or null.                                                                                   |
| [`listKbArticles`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/kb.ts)            | read  | public token | `kb-articles list`      | Knowledge-base articles across every collection, as a flat array; page with `first`/`after` (or `last`/`before`).                                    |
| [`listKbCollections`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/kb.ts)         | read  | public token | `kb-collections list`   | The workspace's knowledge-base collections as a flat array; page with `first`/`after` (or `last`/`before`).                                          |
| [`updateKbArticle`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer       | `kb-articles update`    | Update a knowledge-base article: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                       |
| [`updateKbCollection`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `kb-collections update` | Update a knowledge-base collection: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                    |

**Types:** `KbArticle`, `KbCollection`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListKbArticlesDocument`, `ListKbCollectionsDocument`

## Search

Workspace-wide full-text search.

Guide: [Search](https://developers.uidu.org/docs/domains/search.md).

| Function                                                                               | Kind | Auth         | CLI | Description                                                                                                                  |
| -------------------------------------------------------------------------------------- | ---- | ------------ | --- | ---------------------------------------------------------------------------------------------------------------------------- |
| [`search`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/search.ts) | read | public token | —   | Search the workspace index, optionally limited to `models`, with filters and sorting in `params`; page with `first`/`after`. |

**Types:** `SearchResult`

## Forms

Forms, their questions, and visitor responses.

Guide: [Forms](https://developers.uidu.org/docs/domains/forms.md).

| Function                                                                                          | Kind  | Auth         | CLI            | Description                                                                                                                                              |
| ------------------------------------------------------------------------------------------------- | ----- | ------------ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createForm`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)     | write | Bearer       | `forms create` | Create a form, questions included via `attributes.formQuestionsAttributes`. Returns the payload (with `errors`) or null. The new form is at `form.node`. |
| [`createFormResponse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/forms.ts) | write | Bearer       | —              | Submit a new response to a form; returns the payload as-is, `{ errors, formResponse }`, validation failures included.                                    |
| [`deleteForm`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)     | write | Bearer       | `forms delete` | Delete a form by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                       |
| [`getForm`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/forms.ts)            | read  | public token | `forms get`    | One form with its questions (`inputs`, a connection of `FormQuestion` nodes with field metadata) ready to render, or null.                               |
| [`listForms`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/forms.ts)          | read  | public token | `forms list`   | The workspace's forms as a flat array of summaries, without their questions (use `getForm` for those); page with `first`/`after`.                        |
| [`updateForm`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)     | write | Bearer       | `forms update` | Update a form: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                             |
| [`updateFormResponse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/forms.ts) | write | Bearer       | —              | Update a form response (answers by field value `id`, or a state `event` like `'complete!'`); returns `{ errors, formResponse }` as-is.                   |

**Types:** `Form`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListFormsDocument`

## Events

Events, their instances, and attendances (RSVPs).

Guide: [Events](https://developers.uidu.org/docs/domains/events.md).

| Function                                                                                         | Kind  | Auth         | CLI                  | Description                                                                                                                   |
| ------------------------------------------------------------------------------------------------ | ----- | ------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| [`createAttendance`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/events.ts) | write | Bearer       | `attendances create` | RSVP a contact to an event: `attendableId` is an event *instance* id, never the event id.                                     |
| [`createEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/events.ts)      | write | Bearer       | `events create`      | Create an event; returns the payload as-is, `{ errors, event }`, validation failures included.                                |
| [`getEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/events.ts)         | read  | public token | `events get`         | One event with its primary address, its next or current `instance` and all its `instances`, or null.                          |
| [`listEvents`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/events.ts)       | read  | public token | `events list`        | The workspace's events as a flat array of summaries, each with its next or current `instance`; page with `first`/`after`.     |
| [`updateEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)   | write | Bearer       | `events update`      | Update an event: `{ input: { id, attributes } }` (create it with `createEvent`). Returns the payload (with `errors`) or null. |

**Types:** `Event`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListEventsDocument`

## Donations

Donation campaigns and donations.

Guide: [Donations](https://developers.uidu.org/docs/domains/donations.md).

| Function                                                                                                  | Kind  | Auth         | CLI                | Description                                                                                                                   |
| --------------------------------------------------------------------------------------------------------- | ----- | ------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| [`createDonation`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/donations.ts)         | write | Bearer       | —                  | Record a donation (amount in cents) to a campaign, creating the donor's contact inline; returns `{ errors, donation }` as-is. |
| [`createDonationCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/donations.ts) | write | Bearer       | `donations create` | Create a donation campaign (`goal` in cents); returns the payload as-is, `{ errors, donationCampaign }`.                      |
| [`deleteDonationCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `donations delete` | Delete a donation campaign by `{ input: { id } }`. Returns the payload (with `errors`) or null.                               |
| [`getDonationCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/donations.ts)    | read  | public token | `donations get`    | One donation campaign with its body, its totals (`goal`, `donationsAmount`, in cents) and its donor `form`, or null.          |
| [`listDonationCampaigns`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/donations.ts)  | read  | public token | `donations list`   | The workspace's donation campaigns as a flat array of summaries with their totals in cents; page with `first`/`after`.        |
| [`updateDonationCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer       | `donations update` | Update a donation campaign: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                     |

**Types:** `DonationCampaign`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListDonationCampaignsDocument`

## Courses

Courses, lectures and enrollments.

Guide: [Courses](https://developers.uidu.org/docs/domains/courses.md).

| Function                                                                                         | Kind  | Auth         | CLI              | Description                                                                                                                                      |
| ------------------------------------------------------------------------------------------------ | ----- | ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`createCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses create` | Create a course from `{ input: { attributes } }`. Returns the payload (with `errors`) or null.                                                   |
| [`deleteCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses delete` | Delete a course by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                             |
| [`getCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)       | read  | public token | `courses get`    | One course with its rich-text `body`, or null.                                                                                                   |
| [`getEnrollment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)   | read  | Bearer       | —                | One enrollment with its course, contact and custom fields (`fieldValuesByShortname`), or null.                                                   |
| [`getLecture`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)      | read  | public token | —                | One lecture with its `body`, its course and its `nextLecture` / `previousLecture` for navigation, or null.                                       |
| [`listCourses`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)     | read  | public token | `courses list`   | The workspace's courses as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                    |
| [`listEnrollments`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts) | read  | Bearer       | —                | Course enrollments as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                         |
| [`listLectures`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/courses.ts)    | read  | public token | —                | Lectures across every course as a flat array (filter by `course.id` yourself), with `scopes`, `orderBy`, `from`/`to`; page with `first`/`after`. |
| [`updateCourse`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer       | `courses update` | Update a course: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                   |

**Types:** `Course`, `Enrollment`, `Lecture`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListCoursesDocument`, `ListEnrollmentsDocument`, `ListLecturesDocument`

## Calls

Open calls (grants, applications windows).

Guide: [Calls](https://developers.uidu.org/docs/domains/calls.md).

| Function                                                                                 | Kind | Auth         | CLI          | Description                                                                                          |
| ---------------------------------------------------------------------------------------- | ---- | ------------ | ------------ | ---------------------------------------------------------------------------------------------------- |
| [`getCall`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/calls.ts)   | read | public token | `calls get`  | One call (a job opening or open call) with its application `form`, same shape as `getForm`, or null. |
| [`listCalls`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/calls.ts) | read | public token | `calls list` | The workspace's calls as a flat array of summaries, without their forms; page with `first`/`after`.  |

**Types:** `Call`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListCallsDocument`

## Jobs & applications

Job postings (calls of kind `job`) and the applications to them.

Guide: [Jobs & applications](https://developers.uidu.org/docs/domains/jobs.md).

| Function                                                                                                  | Kind  | Auth         | CLI                   | Description                                                                                                                                                                                                               |
| --------------------------------------------------------------------------------------------------------- | ----- | ------------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`completeApplication`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/applications.ts) | write | Bearer       | —                     | Submit a draft application, the last step of applying.                                                                                                                                                                    |
| [`createApplication`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/applications.ts)   | write | Bearer       | `applications create` | Submit an application to a job. Pass `attributes.applicableId` (the job's id) plus `contactAttributes` and/or `formResponseAttributes`. Returns the mutation payload as-is (including any `errors`) rather than throwing. |
| [`getJob`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/jobs.ts)                      | read  | public token | `jobs get`            | Fetch a single job posting; returns `null` if the call is not a job.                                                                                                                                                      |
| [`listApplications`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/applications.ts)    | read  | Bearer       | `applications list`   | List applications across the workspace (requires a Bearer token).                                                                                                                                                         |
| [`listJobs`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/jobs.ts)                    | read  | public token | `jobs list`           | List job postings (calls of kind `job`).                                                                                                                                                                                  |
| [`updateApplication`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/applications.ts)   | write | Bearer       | —                     | Update an application, optionally firing a state event (`event: 'complete!'`). Returns the mutation payload as-is (including any `errors`) rather than throwing.                                                          |

**Types:** `Application`, `Job`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListApplicationsDocument`

## Contacts & deals

CRM contacts, organisations and deals.

Guide: [Contacts & deals](https://developers.uidu.org/docs/domains/contacts.md).

| Function                                                                                         | Kind  | Auth   | CLI               | Description                                                                                                                                     |
| ------------------------------------------------------------------------------------------------ | ----- | ------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createContact`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `contacts create` | Create a CRM contact from `{ input: { attributes } }`. Returns the payload (with `errors`) or null.                                             |
| [`createDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer | `deals create`    | Create a deal in a pipeline (`attributes.pipelineId`, `statusId`). Returns the payload (with `errors`) or null. The new deal is at `deal.node`. |
| [`deleteContact`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `contacts delete` | Delete a contact by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                           |
| [`getContact`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)     | read  | Bearer | `contacts get`    | One contact with its address and custom fields (`fieldValuesByShortname`), or null; a public token sees none.                                   |
| [`getDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)        | read  | Bearer | `deals get`       | One deal with its pipeline, status and owner (`user`), or null; `amount` is in cents.                                                           |
| [`listContacts`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)   | read  | Bearer | `contacts list`   | Every contact the client may see, as an array: the field is not a connection, so there is no pagination.                                        |
| [`listDeals`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)      | read  | Bearer | `deals list`      | The workspace's deals as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                     |
| [`updateDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | write | Bearer | `deals update`    | Update a deal, e.g. move it to another `statusId`: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                |

**Types:** `Contact`, `Deal`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListDealsDocument`

## Campaigns

Marketing and email campaigns.

Guide: [Campaigns](https://developers.uidu.org/docs/domains/campaigns.md).

| Function                                                                                              | Kind | Auth   | CLI              | Description                                                                                                                                    |
| ----------------------------------------------------------------------------------------------------- | ---- | ------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| [`getCampaign`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/campaigns.ts)        | read | Bearer | `campaigns get`  | One campaign with its `preferences` and target `list`, or null; a public token sees none.                                                      |
| [`listCampaigns`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/campaigns.ts)      | read | Bearer | `campaigns list` | The workspace's campaigns as a flat array, optionally filtered by `kind` and `status`; page with `first`/`after`.                              |
| [`listEmailCampaigns`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/campaigns.ts) | read | Bearer | —                | The email blasts of the workspace's campaigns with their delivery metrics (sent, opened, clicked), as a flat array; page with `first`/`after`. |

**Types:** `Campaign`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListCampaignsDocument`, `ListEmailCampaignsDocument`

## People (HR)

Employees, employments (contracts), compensations, time clocks, benefits and HR reference data.

Guide: [People (HR)](https://developers.uidu.org/docs/domains/people.md).

| Function                                                                                                             | Kind   | Auth   | CLI                          | Description                                                                                                                                                                                                                                                                                                                                                                                                             |
| -------------------------------------------------------------------------------------------------------------------- | ------ | ------ | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`contractEvents`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                       | helper | —      | —                            | Flatten employment history into dated contract events, most recent first: - the employee's earliest contract is a `hire` (assunzione) on its start date; - every later contract is a `variation` (variazione) on its start date; - any contract with an end date yields a `termination` (cessazione) on it.                                                                                                             |
| [`createBenefit`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/benefits.ts)                      | write  | Bearer | `benefits create`            | Create a benefit in the workspace's catalog (name, category, provider, employer/employee costs). Returns the payload (with `errors`) or null; it is at `benefit.node`.                                                                                                                                                                                                                                                  |
| [`createBenefitEnrollment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/benefit-enrollments.ts) | write  | Bearer | `benefit-enrollments create` | Enroll an employment in a benefit (`benefitId`, `employmentId`, `enrolledAt`). Returns the payload (with `errors`) or null; it is at `benefitEnrollment.node`.                                                                                                                                                                                                                                                          |
| [`createCcnl`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                           | write  | Bearer | `ccnls create`               | Create a CCNL (national collective labor contract) — the first-class `Ccnl` record (code, sector, contract levels), distinct from the `Kind`-tagged rows `listCcnls` returns above. `Employment.ccnl` / `ccnlId` point at this first-class type. Requires a Bearer token (`apiKey`).                                                                                                                                    |
| [`createCompensation`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                | write  | Bearer | `compensations create`       | Add a pay item to an employment (`attributes.employmentId`, `amount`, `currency`, `frequency`, `effectiveFrom`). Returns the payload (with `errors`) or null. The new item is at `compensation.node`.                                                                                                                                                                                                                   |
| [`createEmployee`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                    | write  | Bearer | `employees create`           | Create the HR record for a workspace user (`attributes.userId`). Returns the payload (with `errors`) or null. The new employee is at `employee.node`.                                                                                                                                                                                                                                                                   |
| [`createEmployment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                  | write  | Bearer | `employments create`         | Hire: open a new employment (contract) for `attributes.employeeId` with its start date, kind, role and hours. Returns the payload (with `errors`) or null. The new employment is at `employment.node`.                                                                                                                                                                                                                  |
| [`createTimeClock`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                   | write  | Bearer | `time-clocks create`         | Record a time-clock punch (`beginsAt`, optional `finishesAt`, `resourceId`). Returns the payload (with `errors`) or null. The new punch is at `timeClock.node`.                                                                                                                                                                                                                                                         |
| [`deleteCompensation`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                | write  | Bearer | `compensations delete`       | Delete a pay item by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                                                                                  |
| [`deleteEmployment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                  | write  | Bearer | `employments delete`         | Delete an employment by `{ input: { id } }` (to end a contract, use `terminateEmployment` instead). Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                        |
| [`getEmployee`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                          | read   | Bearer | —                            | One employee with current employment, today's time clocks and the shifts of the week starting `weekStartingAt` (see `weekStartFor`), or null.                                                                                                                                                                                                                                                                           |
| [`listCcnls`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                            | read   | Bearer | `ccnls list`                 | List the CCNLs (national collective labor contracts) configured for the workspace — used for an employment's `ccnlId`.                                                                                                                                                                                                                                                                                                  |
| [`listCircles`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                          | read   | Bearer | —                            | List the workspace circles. Circles come in two flavours distinguished by `kind`: org units (e.g. departments — used as an employment's `circleId`) and roles (`kind === 'role'` — used as an employment's `roleId`). Use `listRoles` when you only want the role-Circles.                                                                                                                                              |
| [`listEmployees`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                        | read   | Bearer | `employees list`             | The workspace's employees with their current employment, as a flat array; page with `first`/`after`.                                                                                                                                                                                                                                                                                                                    |
| [`listEmploymentHistory`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                | read   | Bearer | —                            | List every employee with their full employment (contract) history — not just the current one. Use `contractEvents` to turn this into a flat, dated list of hires, terminations and contract variations.                                                                                                                                                                                                                 |
| [`listKinds`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                            | read   | Bearer | —                            | List `Kind` records for the given polymorphic models.                                                                                                                                                                                                                                                                                                                                                                   |
| [`listOffices`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                          | read   | Bearer | `offices list`               | List the workspace offices — used for an employment's `employmentOfficesAttributes`.                                                                                                                                                                                                                                                                                                                                    |
| [`listRoles`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                            | read   | Bearer | `roles list`                 | List only the role-Circles (structured job titles) — an employment's `roleId`.                                                                                                                                                                                                                                                                                                                                          |
| [`terminateEmployment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)               | write  | Bearer | —                            | Terminate (cessazione) an employment by closing it with an end date and reason. Thin semantic wrapper over `updateEmployment`. `endReason` is one of: resignation, termination, contract\_end, retirement, other, resignation\_just\_cause, dismissal\_just\_cause, dismissal\_subjective\_reason, dismissal\_objective\_reason, collective\_dismissal, mutual\_agreement, probation\_failed, death, business\_closure. |
| [`updateCompensation`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                | write  | Bearer | `compensations update`       | Update a pay item: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                                                                        |
| [`updateEmployee`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                    | write  | Bearer | `employees update`           | Update an employee's HR record (code, kiosk PIN, time-clock flags): `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                       |
| [`updateEmployment`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)                  | write  | Bearer | `employments update`         | Update an employment: `{ input: { id, attributes } }`; to end one, prefer `terminateEmployment`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                                                                           |
| [`weekStartFor`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/people.ts)                         | helper | —      | —                            | Compute the Monday of the week containing `date` as an ISO8601 date string. Useful for the `weekStartingAt` argument required by `getEmployee` (and any other weekly-aggregated employee fields).                                                                                                                                                                                                                       |

**Types:** `Circle`, `CompensationRecord`, `ContractEvent`, `ContractEventType`, `Employee`, `EmployeeContracts`, `EmploymentRecord`, `Kind`, `Office`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListEmployeesDocument`

## Goals (OKRs)

Objectives, key results and timeframes, plus value/progress helpers.

Guide: [Goals (OKRs)](https://developers.uidu.org/docs/domains/goals.md).

| 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`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListGoalsDocument`, `ListTimeframesDocument`

## Spaces, tasks & notes

Collaboration primitives: spaces, tasks, notes.

Guide: [Spaces, tasks & notes](https://developers.uidu.org/docs/domains/workspace.md).

| Function                                                                                       | Kind  | Auth   | CLI             | Description                                                                                                                 |
| ---------------------------------------------------------------------------------------------- | ----- | ------ | --------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [`createNote`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/notes.ts)      | write | Bearer | `notes create`  | Create a note. Returns the payload (with `errors`) or null; the new note is at `note.node`.                                 |
| [`createSpace`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/spaces.ts)    | write | Bearer | `spaces create` | Create a space, the container tasks live in. Returns the payload (with `errors`) or null; the new space is at `space.node`. |
| [`createTask`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/tasks.ts)      | write | Bearer | `tasks create`  | Create a task, usually in a space (`attributes.spaceId`). Returns the payload (with `errors`) or null.                      |
| [`deleteNote`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer | `notes delete`  | Delete a note by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                          |
| [`deleteSpace`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `spaces delete` | Delete a space by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                         |
| [`deleteTask`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer | `tasks delete`  | Delete a task by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                          |
| [`updateNote`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer | `notes update`  | Update a note: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                |
| [`updateSpace`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `spaces update` | Update a space: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                               |
| [`updateTask`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)  | write | Bearer | `tasks update`  | Update a task, e.g. its `statusId` or dates: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.  |

## Bookings & calendars

Bookings, calendars and calendar events.

Guide: [Bookings & calendars](https://developers.uidu.org/docs/domains/bookings.md).

| Function                                                                                             | Kind | Auth         | CLI             | Description                                                                                                                            |
| ---------------------------------------------------------------------------------------------------- | ---- | ------------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| [`getBooking`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)         | read | Bearer       | `bookings get`  | One booking with its contact, totals in cents and custom fields (`fieldValuesByShortname`), or null; a public token sees none.         |
| [`getCalendar`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)        | read | public token | —               | One calendar (name, color, time zone), or null.                                                                                        |
| [`getCalendarEvent`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)   | read | public token | —               | One calendar event with its rich-text `body` and its calendar, or null.                                                                |
| [`listBookings`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)       | read | Bearer       | `bookings list` | The workspace's bookings as a flat array; page with `first`/`after` (or `last`/`before`); a public token gets an empty list.           |
| [`listCalendarEvents`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts) | read | public token | —               | Calendar events as a flat array, optionally narrowed by `calendarIds` and named `scopes` (e.g. `upcoming`); page with `first`/`after`. |
| [`listCalendars`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/bookings.ts)      | read | public token | —               | The workspace's calendars as a flat array; page with `first`/`after` (or `last`/`before`).                                             |

**Types:** `Booking`, `Calendar`, `CalendarEvent`

**Documents** (for [`paginate`](https://developers.uidu.org/docs/recipes/paginate.md)): `ListBookingsDocument`, `ListCalendarEventsDocument`, `ListCalendarsDocument`

## Models (custom data)

Define your own record types (Models, Fields) and store ModelItems.

Guide: [Models (custom data)](https://developers.uidu.org/docs/domains/models.md).

| Function                                                                                                | Kind   | Auth   | CLI | Description                                                                                                                                                                                                                                                                                              |
| ------------------------------------------------------------------------------------------------------- | ------ | ------ | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createField`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Pass `fieldableId` (the model) with `fieldableType: 'Model'`: a custom app may only name a type that matches the record it loads.                                                                                                                                                                        |
| [`createFieldOption`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)       | write  | Bearer | —   | Add an option to a select-kind field (`attributes.fieldId`, `name`, `color`). Returns the payload (with `errors`) or null.                                                                                                                                                                               |
| [`createModel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Create a model in a WorkspaceApp (`attributes.modelableId`, `name`, `namePlural`). Returns the payload (with `errors`) or null.                                                                                                                                                                          |
| [`createModelItem`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)         | write  | Bearer | —   | Pass `attributes.modelId` and `attributes.fieldValuesAttributes` — build the latter by shortname with `toFieldValuesAttributes`.                                                                                                                                                                         |
| [`deleteField`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Delete a field from its model by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                       |
| [`deleteFieldOption`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)       | write  | Bearer | —   | Delete a select option by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                              |
| [`deleteFieldValue`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)        | write  | Bearer | —   | Clear a single value by its FieldValue id; `deleteLinkedRecord: true` also deletes the record it links to. Returns the payload (with `errors`) or null.                                                                                                                                                  |
| [`deleteModelItem`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)         | write  | Bearer | —   | Delete an item by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                                      |
| [`ensureModel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Find the app's model by name, or create it — and any of `fields` it lacks. Returns the model with all its fields. What a custom app runs on its first load until schemas come from an install-time manifest (custom apps spec, step 3). Throws a `UiduError` (`MUTATION_ERROR`) when a write is refused. |
| [`getModel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)                | read   | Bearer | —   | One model with its fields, or null when the id is not a Model.                                                                                                                                                                                                                                           |
| [`getModelItem`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)            | read   | Bearer | —   | One model item with its values and its `model.id`, or null when the id is not a ModelItem.                                                                                                                                                                                                               |
| [`listModelItems`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)          | read   | Bearer | —   | A model's items. Each carries `fieldValuesByShortname` — `{ room: 'Aula Magna', when: '…' }` — the easiest shape to read values from.                                                                                                                                                                    |
| [`listModels`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)              | read   | Bearer | —   | The models of a WorkspaceApp, with their fields.                                                                                                                                                                                                                                                         |
| [`toFieldValuesAttributes`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts) | helper | —      | —   | `{ room: 'Aula Magna' }` → `[{ fieldId: '<id of room>', content: { value: 'Aula Magna' } }]`, for `createModelItem` / `updateModelItem`. Throws on a shortname the model doesn't have; skips `undefined` values.                                                                                         |
| [`updateField`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Update a field (name, `required`, preferences…): `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                           |
| [`updateFieldOption`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)       | write  | Bearer | —   | Update a select option: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                                    |
| [`updateFieldValue`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)        | write  | Bearer | —   | Set a single value by its FieldValue id: `{ input: { id, attributes: { content: { value } } } }`. Returns the payload (with `errors`) or null.                                                                                                                                                           |
| [`updateModel`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)             | write  | Bearer | —   | Rename or reconfigure a model: `{ input: { id, attributes } }`. Returns the payload (with `errors`) or null.                                                                                                                                                                                             |
| [`updateModelItem`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/models.ts)         | write  | Bearer | —   | Update an item: `{ input: { id, attributes: { fieldValuesAttributes } } }`, built with `toFieldValuesAttributes`. Returns the payload (with `errors`) or null.                                                                                                                                           |

**Types:** `EnsureModelOptions`, `Model`, `ModelField`, `ModelFieldOption`, `ModelItem`

## Provisioning & actions

Create workspaces, rotate API credentials, and run any tool-flagged ActionKey.

Guide: [Provisioning & actions](https://developers.uidu.org/docs/foundations/writes.md).

| Function                                                                                                        | Kind  | Auth   | CLI                     | Description                                                                                                                                                 |
| --------------------------------------------------------------------------------------------------------------- | ----- | ------ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createWorkspace`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/system.ts)                 | write | Bearer | `workspace create`      | Create a workspace owned by the authenticated account (`attributes.name`, `subdomain`…). Returns the payload (with `errors`) or null.                       |
| [`executeAction`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/actions.ts)                  | write | Bearer | `tools call`            | Run any backend action by `key` (an `ActionKey` flagged `of_kind_tool_key`), with optional `id` and `attributes`; returns `{ result, errors }` or null.     |
| [`generateWorkspaceApiCredentials`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/system.ts) | write | Bearer | `workspace credentials` | Rotate + return a workspace's API credentials (apiKey/apiSecret). Operates on the current workspace. Use the returned credentials to wire a scaffolded app. |

---

# @uidu/react reference

URL: https://developers.uidu.org/docs/reference/react

> Every export of @uidu/react, generated from the source.

React bindings for `@uidu/client`. Guides: [Getting started](https://developers.uidu.org/docs/tools/react/getting-started.md), [Components](https://developers.uidu.org/docs/tools/react/components.md), [Hooks](https://developers.uidu.org/docs/tools/react/hooks.md).

## Components

| Export                                                                                                          | Description                                                                                                                                                                                                                                                          | Demo                                                                                                                                                          |
| --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`BlockRenderer`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/components/BlockRenderer.tsx) | Render exactly one block from a page's `pageBlocks` array, by shortname.                                                                                                                                                                                             | —                                                                                                                                                             |
| [`DynamicForm`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/components/DynamicForm.tsx)     | Render a uidu form (from `getForm`, `call.form`, …) as HTML inputs and hand the parsed values to your `action`, typically a `createFormResponse` Server Action.                                                                                                      | [`app/forms/[id]/FormWithReceipt.tsx`](https://github.com/uidu-org/api.js/blob/main/apps/demo/src/app/forms/\[id]/FormWithReceipt.tsx)                        |
| [`PageBlocks`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/components/PageBlocks.tsx)       | Render a page's blocks in order, each with the component keyed by its shortname in `components`; blocks with no match render `fallback`, or nothing.                                                                                                                 | [`app/page.tsx`](https://github.com/uidu-org/api.js/blob/main/apps/demo/src/app/page.tsx)                                                                     |
| [`RichText`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/components/RichText.tsx)           | Render a Tiptap (ProseMirror) JSON document as React elements.                                                                                                                                                                                                       | [`components/themes/careers/show/ClassicShow.tsx`](https://github.com/uidu-org/api.js/blob/main/apps/demo/src/components/themes/careers/show/ClassicShow.tsx) |
| [`UiduAppProvider`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/providers/app.tsx)          | Connects a custom app to the uidu page framing it (`@uidu/app-bridge`) and hands the tree its state through `useUiduApp`. Once connected, the children also sit inside a `<UiduProvider>` with a session-backed client, so `useUiduClient()` works as anywhere else. | [`app/custom-app/page.tsx`](https://github.com/uidu-org/api.js/blob/main/apps/demo/src/app/custom-app/page.tsx)                                               |
| [`UiduProvider`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/providers/index.tsx)           | Provide a `UiduClient` to the tree, either a `client` you built or one made from `workspace`/`publicToken`/`endpoint`, plus an SWR fetcher backed by it.                                                                                                             | —                                                                                                                                                             |

## Hooks

| Export                                                                                                                                                                                                                                                 | Description                                                                                                                                                                                     | Demo                                                                                                                                |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| [`useFields`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/hooks/useFields.ts)<br />`useFields(fieldValues: ReadonlyArray<FieldValue> \| null \| undefined): Record<string, unknown>`                                               | Normalize a `fieldValues` array into a `{ shortname: value }` map so blocks can access their fields by name without walking the GraphQL shape.                                                  | [`components/blocks/ContactForm.tsx`](https://github.com/uidu-org/api.js/blob/main/apps/demo/src/components/blocks/ContactForm.tsx) |
| [`useQuery`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/hooks/useQuery.ts)<br />`useQuery(document: TypedDocumentNode<TResult, TVariables>, variables?: TVariables, options?: UseQueryOptions<TResult>): UseQueryResult<TResult>` | Generic client-side data-fetching hook: runs `document` against the `UiduClient` from the nearest `<UiduProvider>` via SWR, so calls are cached and deduplicated by operation name + variables. | —                                                                                                                                   |
| [`useUidu`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/providers/index.tsx)<br />`useUidu(): UiduContextValue`                                                                                                                    | The nearest `UiduProvider`'s `{ client, endpoint }`; throws outside a provider.                                                                                                                 | —                                                                                                                                   |
| [`useUiduApp`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/providers/app.tsx)<br />`useUiduApp(): UiduAppState`                                                                                                                    | The custom app's bridge state: `connecting`, `ready` or `error`.                                                                                                                                | [`app/custom-app/page.tsx`](https://github.com/uidu-org/api.js/blob/main/apps/demo/src/app/custom-app/page.tsx)                     |
| [`useUiduClient`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/providers/index.tsx)<br />`useUiduClient(): UiduClient`                                                                                                              | The `UiduClient` from the nearest `UiduProvider`; throws outside a provider.                                                                                                                    | —                                                                                                                                   |

## Functions

| Export                                                                                                                                                                                                                       | Description                                                                                                                                                                                                                        | Demo                                                                                                                                |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| [`formatSalaryRange`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/utils/jobs.ts)<br />`formatSalaryRange(job: Pick<Job, 'salaryMin' \| 'salaryMax'>, options: FormatSalaryOptions = {}): string \| null` | Format a job's salary range (`salaryMin`/`salaryMax`) into a display string. Returns `null` when the job advertises no salary, so callers can hide the field. Handles open-ended ranges ("From …", "Up to …") and a single figure. | —                                                                                                                                   |
| [`getBlockShortname`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/components/PageBlocks.tsx)<br />`getBlockShortname(block: PageBlock): string \| null`                                                  | The shortname a page block is rendered by (its template block's, else its own), or null.                                                                                                                                           | —                                                                                                                                   |
| [`toText`](https://github.com/uidu-org/api.js/blob/main/packages/react/src/utils/text.ts)<br />`toText(value: unknown): string`                                                                                              | Flatten any uidu field-content value into a plain string. Handles raw strings, numbers, Slate-style rich text nodes (`{ type, children }`, `{ text }`), arrays of nodes, and wrapper objects with a `value` field.                 | [`components/blocks/ContactForm.tsx`](https://github.com/uidu-org/api.js/blob/main/apps/demo/src/components/blocks/ContactForm.tsx) |

## Types

`BlockComponent`, `BlockRendererProps`, `DynamicFormProps`, `DynamicFormResult`, `DynamicFormValues`, `FormatSalaryOptions`, `PageBlocksProps`, `RichTextNode`, `RichTextProps`, `UiduAppProviderProps`, `UiduAppState`, `UseQueryOptions`, `UseQueryResult`

---

# Roadmap

URL: https://developers.uidu.org/docs/reference/roadmap

> What the uidu platform can do that the SDK does not cover yet — generated from the platform’s action registry.

The platform exposes **202 actions** to tools and agents. The SDK covers **23** fully — a typed `@uidu/client` function, a CLI command and a demo page — and **15** partly; **158** have no SDK layer yet, and **6** are generic tools with no entity to type.

<Callout title="Not covered ≠ not reachable">
  Every action below can be run today, with a Bearer token, through the generic dispatcher: `executeAction(client, { input: { key, attributes } })` in `@uidu/client` (plus `id` for an update), or `uidu tools call <key> --attributes '<json>'` from the CLI (creates only: the CLI can't pass an `id` yet). Associations are GlobalIDs. What the SDK adds is the typed function, the CLI noun, a demo and these docs.
</Callout>

Store (products, SKUs, orders) and memberships (subscriptions) are here: the platform has them, the SDK does not yet. Snapshot of the registry taken 2026-10-08.

## Accounting

0 of 16 covered.

| Action                             | Key                                                             | Client | CLI | Demo |
| ---------------------------------- | --------------------------------------------------------------- | :----: | :-: | :--: |
| Create activity                    | `Activities::Activity::Create`                                  |    —   |  —  |   —  |
| Create analytic account allocation | `AnalyticAccountAllocations::AnalyticAccountAllocation::Create` |    —   |  —  |   —  |
| Create analytic account            | `AnalyticAccounts::AnalyticAccount::Create`                     |    —   |  —  |   —  |
| Create bank account                | `BankAccounts::BankAccount::Create`                             |    —   |  —  |   —  |
| Create bank transaction            | `BankTransactions::BankTransaction::Create`                     |    —   |  —  |   —  |
| Create budget                      | `Budgets::Budget::Create`                                       |    —   |  —  |   —  |
| Create budget entry                | `Budgets::BudgetEntry::Create`                                  |    —   |  —  |   —  |
| Create budget line                 | `Budgets::BudgetLine::Create`                                   |    —   |  —  |   —  |
| Create credit line                 | `CreditLines::CreditLine::Create`                               |    —   |  —  |   —  |
| Create invoice                     | `Invoices::Invoice::Create`                                     |    —   |  —  |   —  |
| Create management account          | `ManagementAccounts::ManagementAccount::Create`                 |    —   |  —  |   —  |
| Update management account          | `ManagementAccounts::ManagementAccount::Update`                 |    —   |  —  |   —  |
| Create purchase                    | `Purchases::Purchase::Create`                                   |    —   |  —  |   —  |
| Create purchase order              | `Purchases::PurchaseOrder::Create`                              |    —   |  —  |   —  |
| Create purchase request            | `Purchases::PurchaseRequest::Create`                            |    —   |  —  |   —  |
| Create transaction kind            | `TransactionKinds::TransactionKind::Create`                     |    —   |  —  |   —  |

## Agenda

0 of 3 covered.

| Action                | Key                                 | Client | CLI | Demo |
| --------------------- | ----------------------------------- | :----: | :-: | :--: |
| Create booking link   | `BookingLinks::BookingLink::Create` |    —   |  —  |   —  |
| Create calendar event | `Calendars::CalendarEvent::Create`  |    —   |  —  |   ✓  |
| Update calendar event | `Calendars::CalendarEvent::Update`  |    —   |  —  |   ✓  |

## Approvals

0 of 2 covered.

| Action               | Key                               | Client | CLI | Demo |
| -------------------- | --------------------------------- | :----: | :-: | :--: |
| Create approval form | `Approvals::ApprovalForm::Create` |    —   |  —  |   —  |
| Update approval form | `Approvals::ApprovalForm::Update` |    —   |  —  |   —  |

## Calls

0 of 3 covered.

| Action           | Key                        | Client | CLI | Demo |
| ---------------- | -------------------------- | :----: | :-: | :--: |
| Create interview | `Calls::Interview::Create` |    —   |  —  |   —  |
| Update interview | `Calls::Interview::Update` |    —   |  —  |   —  |
| Update call      | `Calls::Call::Update`      |    —   |  —  |   ✓  |

## Campaigns

0 of 2 covered.

| Action      | Key                       | Client | CLI | Demo |
| ----------- | ------------------------- | :----: | :-: | :--: |
| Create list | `Campaigns::List::Create` |    —   |  —  |   —  |
| Update list | `Campaigns::List::Update` |    —   |  —  |   —  |

## CMS

1 of 4 covered.

| Action              | Key                          | Client | CLI | Demo |
| ------------------- | ---------------------------- | :----: | :-: | :--: |
| Create website page | `Sites::WebsitePage::Create` |    —   |  —  |   —  |
| Update website page | `Sites::WebsitePage::Update` |    —   |  —  |   —  |
| Create project      | `Projects::Project::Create`  |    ✓   |  —  |   ✓  |
| Create site         | `Sites::Site::Create`        |    ✓   |  ✓  |   ✓  |

## Compliance

0 of 6 covered.

| Action                            | Key                                             |    Client    | CLI | Demo |
| --------------------------------- | ----------------------------------------------- | :----------: | :-: | :--: |
| Create obligation                 | `Compliance::Obligation::Create`                |       —      |  —  |   —  |
| Update obligation                 | `Compliance::Obligation::Update`                |       —      |  —  |   —  |
| Create obligation acknowledgement | `Compliance::ObligationAcknowledgement::Create` |       —      |  —  |   —  |
| Create obligation assignment      | `Compliance::ObligationAssignment::Create`      |       —      |  —  |   —  |
| Exempt obligation assignment      | `Compliance::ObligationAssignment::Exempt`      | `tools call` |  —  |   —  |
| Reopen obligation assignment      | `Compliance::ObligationAssignment::Reopen`      | `tools call` |  —  |   —  |

## Conversations

0 of 2 covered.

| Action              | Key                                   | Client | CLI | Demo |
| ------------------- | ------------------------------------- | :----: | :-: | :--: |
| Create conversation | `Conversations::Conversation::Create` |    —   |  —  |   —  |
| Create email        | `Emails::Email::Create`               |    —   |  —  |   —  |

## Core

0 of 11 covered.

| Action                      | Key                                         | Client | CLI | Demo |
| --------------------------- | ------------------------------------------- | :----: | :-: | :--: |
| Create field group          | `FieldGroups::FieldGroup::Create`           |    —   |  —  |   —  |
| Update field group          | `FieldGroups::FieldGroup::Update`           |    —   |  —  |   —  |
| Create incoming webhook     | `IncomingWebhooks::IncomingWebhook::Create` |    —   |  —  |   —  |
| Create journal entry        | `JournalEntries::JournalEntry::Create`      |    —   |  —  |   —  |
| Read a file's text          | `Parsings::Parsing::Create`                 |    —   |  —  |   —  |
| Create signature request    | `Signatures::SignatureRequest::Create`      |    —   |  —  |   —  |
| Create workspace connection | `System::WorkspaceConnection::Create`       |    —   |  —  |   —  |
| Update workspace connection | `System::WorkspaceConnection::Update`       |    —   |  —  |   —  |
| Create workspace tag        | `WorkspaceTags::WorkspaceTag::Create`       |    —   |  —  |   —  |
| Update workspace tag        | `WorkspaceTags::WorkspaceTag::Update`       |    —   |  —  |   —  |
| Create space                | `Spaces::Space::Create`                     |    ✓   |  ✓  |   —  |

## Courses

2 of 6 covered.

| Action                | Key                              | Client | CLI | Demo |
| --------------------- | -------------------------------- | :----: | :-: | :--: |
| Create lecture module | `Courses::LectureModule::Create` |    —   |  —  |   —  |
| Update lecture module | `Courses::LectureModule::Update` |    —   |  —  |   —  |
| Create lecture        | `Courses::Lecture::Create`       |    —   |  —  |   ✓  |
| Update lecture        | `Courses::Lecture::Update`       |    —   |  —  |   ✓  |
| Create course         | `Courses::Course::Create`        |    ✓   |  ✓  |   ✓  |
| Update course         | `Courses::Course::Update`        |    ✓   |  ✓  |   ✓  |

## Cross-app

2 of 32 covered.

| Action                       | Key                                           | Client | CLI | Demo |
| ---------------------------- | --------------------------------------------- | :----: | :-: | :--: |
| Create availability          | `Availabilities::Availability::Create`        |    —   |  —  |   —  |
| Create billing profile       | `BillingProfiles::BillingProfile::Create`     |    —   |  —  |   —  |
| Create certification         | `Certifications::Certification::Create`       |    —   |  —  |   —  |
| Create channel kb collection | `Channels::ChannelKbCollection::Create`       |    —   |  —  |   —  |
| Update channel kb collection | `Channels::ChannelKbCollection::Update`       |    —   |  —  |   —  |
| Create contract              | `Contracts::Contract::Create`                 |    —   |  —  |   —  |
| Create contract commitment   | `Contracts::ContractCommitment::Create`       |    —   |  —  |   —  |
| Create contract item         | `Contracts::ContractItem::Create`             |    —   |  —  |   —  |
| Create coupon                | `Coupons::Coupon::Create`                     |    —   |  —  |   —  |
| Create message thread        | `MessageThreads::MessageThread::Create`       |    —   |  —  |   —  |
| Create name format part      | `NameFormats::NameFormatPart::Create`         |    —   |  —  |   —  |
| Update name format part      | `NameFormats::NameFormatPart::Update`         |    —   |  —  |   —  |
| Create payment               | `Payments::Payment::Create`                   |    —   |  —  |   —  |
| Create pipeline              | `Pipelines::Pipeline::Create`                 |    —   |  —  |   —  |
| Create status                | `Pipelines::Status::Create`                   |    —   |  —  |   —  |
| Update status                | `Pipelines::Status::Update`                   |    —   |  —  |   —  |
| Create price                 | `Prices::Price::Create`                       |    —   |  —  |   —  |
| Create product               | `Products::Product::Create`                   |    —   |  —  |   —  |
| Create scheduled message     | `ScheduledMessages::ScheduledMessage::Create` |    —   |  —  |   —  |
| Create shift                 | `Shifts::Shift::Create`                       |    —   |  —  |   —  |
| Create sku                   | `Skus::Sku::Create`                           |    —   |  —  |   —  |
| Create subscription          | `Subscriptions::Subscription::Create`         |    —   |  —  |   —  |
| Create tax rate              | `TaxRates::TaxRate::Create`                   |    —   |  —  |   —  |
| Create venue                 | `Venues::Venue::Create`                       |    —   |  —  |   —  |
| Update venue                 | `Venues::Venue::Update`                       |    —   |  —  |   —  |
| Create venue reservation     | `Venues::VenueReservation::Create`            |    —   |  —  |   —  |
| Update venue reservation     | `Venues::VenueReservation::Update`            |    —   |  —  |   —  |
| Create field option          | `FieldOptions::FieldOption::Create`           |    ✓   |  —  |   —  |
| Update field option          | `FieldOptions::FieldOption::Update`           |    ✓   |  —  |   —  |
| Create order                 | `Orders::Order::Create`                       |    —   |  —  |   ✓  |
| Create attendance            | `Attendances::Attendance::Create`             |    ✓   |  ✓  |   ✓  |
| Create channel               | `Channels::Channel::Create`                   |    ✓   |  ✓  |   ✓  |

## Decisions

0 of 2 covered.

| Action                   | Key                                   | Client | CLI | Demo |
| ------------------------ | ------------------------------------- | :----: | :-: | :--: |
| Create decision          | `Decisions::Decision::Create`         |    —   |  —  |   —  |
| Create decision response | `Decisions::DecisionResponse::Create` |    —   |  —  |   —  |

## Documents

0 of 9 covered.

| Action                   | Key                                       | Client | CLI | Demo |
| ------------------------ | ----------------------------------------- | :----: | :-: | :--: |
| Update blob              | `Blobs::Blob::Update`                     |    —   |  —  |   —  |
| Say what a document is   | `Classifications::Classification::Create` |    —   |  —  |   —  |
| Create document          | `Documents::Document::Create`             |    —   |  —  |   —  |
| Update document          | `Documents::Document::Update`             |    —   |  —  |   —  |
| Create document project  | `Documents::DocumentProject::Create`      |    —   |  —  |   —  |
| Update document project  | `Documents::DocumentProject::Update`      |    —   |  —  |   —  |
| Create folder            | `Documents::Folder::Create`               |    —   |  —  |   —  |
| Update folder            | `Documents::Folder::Update`               |    —   |  —  |   —  |
| Read a document's fields | `Extractions::Extraction::Create`         |    —   |  —  |   —  |

## Donations

2 of 2 covered.

| Action                   | Key                                           | Client | CLI | Demo |
| ------------------------ | --------------------------------------------- | :----: | :-: | :--: |
| Create donation campaign | `DonationCampaigns::DonationCampaign::Create` |    ✓   |  ✓  |   ✓  |
| Create donation          | `Donations::Donation::Create`                 |    ✓   |  ✓  |   ✓  |

## Events

2 of 7 covered.

| Action                  | Key                               | Client | CLI | Demo |
| ----------------------- | --------------------------------- | :----: | :-: | :--: |
| Update event instance   | `Events::EventInstance::Update`   |    —   |  —  |   —  |
| Create sponsor          | `Events::Sponsor::Create`         |    —   |  —  |   —  |
| Update sponsor          | `Events::Sponsor::Update`         |    —   |  —  |   —  |
| Create sponsorship plan | `Events::SponsorshipPlan::Create` |    —   |  —  |   —  |
| Update sponsorship plan | `Events::SponsorshipPlan::Update` |    —   |  —  |   —  |
| Create event            | `Events::Event::Create`           |    ✓   |  ✓  |   ✓  |
| Update event            | `Events::Event::Update`           |    ✓   |  ✓  |   ✓  |

## Expenses

0 of 2 covered.

| Action                | Key                               | Client | CLI | Demo |
| --------------------- | --------------------------------- | :----: | :-: | :--: |
| Create expense        | `Expenses::Expense::Create`       |    —   |  —  |   —  |
| Create expense report | `Expenses::ExpenseReport::Create` |    —   |  —  |   —  |

## Fieldservice

0 of 5 covered.

| Action                      | Key                                      | Client | CLI | Demo |
| --------------------------- | ---------------------------------------- | :----: | :-: | :--: |
| Create commitment slot      | `Contracts::CommitmentSlot::Create`      |    —   |  —  |   —  |
| Create coverage requirement | `Contracts::CoverageRequirement::Create` |    —   |  —  |   —  |
| Create work order           | `WorkOrders::WorkOrder::Create`          |    —   |  —  |   —  |
| Update work order           | `WorkOrders::WorkOrder::Update`          |    —   |  —  |   —  |
| Create work site            | `WorkSites::WorkSite::Create`            |    —   |  —  |   —  |

## Forms

2 of 8 covered.

| Action               | Key                           | Client | CLI | Demo |
| -------------------- | ----------------------------- | :----: | :-: | :--: |
| Create form page     | `Forms::FormPage::Create`     |    —   |  —  |   —  |
| Update form page     | `Forms::FormPage::Update`     |    —   |  —  |   —  |
| Create form question | `Forms::FormQuestion::Create` |    —   |  —  |   —  |
| Create form scoring  | `Forms::FormScoring::Create`  |    —   |  —  |   —  |
| Update form scoring  | `Forms::FormScoring::Update`  |    —   |  —  |   —  |
| Update scoring rule  | `Forms::ScoringRule::Update`  |    —   |  —  |   —  |
| Create form          | `Forms::Form::Create`         |    ✓   |  ✓  |   ✓  |
| Update form          | `Forms::Form::Update`         |    ✓   |  ✓  |   ✓  |

## Goals

0 of 3 covered.

| Action           | Key                             | Client | CLI | Demo |
| ---------------- | ------------------------------- | :----: | :-: | :--: |
| Create cadence   | `Cadences::Cadence::Create`     |    —   |  —  |   —  |
| Create goal      | `Goals::Goal::Create`           |    —   |  —  |   —  |
| Create timeframe | `Timeframes::Timeframe::Create` |    —   |  —  |   —  |

## Grants

0 of 1 covered.

| Action       | Key                     | Client | CLI | Demo |
| ------------ | ----------------------- | :----: | :-: | :--: |
| Create grant | `Grants::Grant::Create` |    —   |  —  |   —  |

## Help center

0 of 7 covered.

| Action               | Key                                |    Client    | CLI | Demo |
| -------------------- | ---------------------------------- | :----------: | :-: | :--: |
| Create ticket        | `Tickets::Ticket::Create`          |       —      |  —  |   —  |
| Update ticket        | `Tickets::Ticket::Update`          |       —      |  —  |   —  |
| Snooze ticket        | `Tickets::Ticket::Snooze`          | `tools call` |  —  |   —  |
| Create kb article    | `HelpCenter::KbArticle::Create`    |       ✓      |  ✓  |   —  |
| Update kb article    | `HelpCenter::KbArticle::Update`    |       ✓      |  ✓  |   —  |
| Create kb collection | `HelpCenter::KbCollection::Create` |       ✓      |  ✓  |   —  |
| Update kb collection | `HelpCenter::KbCollection::Update` |       ✓      |  ✓  |   —  |

## Inventory

0 of 11 covered.

| Action                    | Key                                           | Client | CLI | Demo |
| ------------------------- | --------------------------------------------- | :----: | :-: | :--: |
| Create carrier            | `Carriers::Carrier::Create`                   |    —   |  —  |   —  |
| Update carrier            | `Carriers::Carrier::Update`                   |    —   |  —  |   —  |
| Create inventory item     | `InventoryItems::InventoryItem::Create`       |    —   |  —  |   —  |
| Create inventory project  | `InventoryProjects::InventoryProject::Create` |    —   |  —  |   —  |
| Create receipt            | `Receipts::Receipt::Create`                   |    —   |  —  |   —  |
| Create transfer           | `Transfers::Transfer::Create`                 |    —   |  —  |   —  |
| Create warehouse          | `Warehouses::Warehouse::Create`               |    —   |  —  |   —  |
| Create warehouse location | `Warehouses::WarehouseLocation::Create`       |    —   |  —  |   —  |
| Update warehouse location | `Warehouses::WarehouseLocation::Update`       |    —   |  —  |   —  |
| Create warehouse sku      | `Warehouses::WarehouseSku::Create`            |    —   |  —  |   —  |
| Update warehouse sku      | `Warehouses::WarehouseSku::Update`            |    —   |  —  |   —  |

## Jobs

0 of 4 covered.

| Action              | Key                           |    Client    | CLI | Demo |
| ------------------- | ----------------------------- | :----------: | :-: | :--: |
| Create candidate    | `Calls::Candidate::Create`    |       —      |  —  |   —  |
| Update candidate    | `Calls::Candidate::Update`    |       —      |  —  |   —  |
| Search candidates   | `Ai::CandidateSearch`         | `tools call` |  —  |   —  |
| Propose application | `Calls::Application::Propose` | `tools call` |  —  |   —  |

## Meetings

0 of 1 covered.

| Action         | Key                         | Client | CLI | Demo |
| -------------- | --------------------------- | :----: | :-: | :--: |
| Create meeting | `Meetings::Meeting::Create` |    —   |  —  |   —  |

## Notes

0 of 1 covered.

| Action      | Key                   | Client | CLI | Demo |
| ----------- | --------------------- | :----: | :-: | :--: |
| Create note | `Notes::Note::Create` |    ✓   |  ✓  |   —  |

## Offices

0 of 3 covered.

| Action               | Key                             | Client | CLI | Demo |
| -------------------- | ------------------------------- | :----: | :-: | :--: |
| Create office        | `Offices::Office::Create`       |    —   |  —  |   —  |
| Create venue booking | `Offices::VenueBooking::Create` |    —   |  —  |   —  |
| Update venue booking | `Offices::VenueBooking::Update` |    —   |  —  |   —  |

## People

7 of 26 covered.

| Action                            | Key                                                           |    Client    | CLI | Demo |
| --------------------------------- | ------------------------------------------------------------- | :----------: | :-: | :--: |
| Create employee certification     | `EmployeeCertifications::EmployeeCertification::Create`       |       —      |  —  |   —  |
| Create employment cost rate       | `EmploymentCostRates::EmploymentCostRate::Create`             |       —      |  —  |   —  |
| Create employment kind            | `EmploymentKinds::EmploymentKind::Create`                     |       —      |  —  |   —  |
| Create kiosk                      | `Kiosks::Kiosk::Create`                                       |       —      |  —  |   —  |
| Create onboarding                 | `Onboardings::Onboarding::Create`                             |       —      |  —  |   —  |
| Create skill                      | `People::Skill::Create`                                       |       —      |  —  |   —  |
| Update skill                      | `People::Skill::Update`                                       |       —      |  —  |   —  |
| Create workspace holiday          | `People::WorkspaceHoliday::Create`                            |       —      |  —  |   —  |
| Update workspace holiday          | `People::WorkspaceHoliday::Update`                            |       —      |  —  |   —  |
| Create workspace skill            | `People::WorkspaceSkill::Create`                              |       —      |  —  |   —  |
| Update workspace skill            | `People::WorkspaceSkill::Update`                              |       —      |  —  |   —  |
| Create position                   | `Positions::Position::Create`                                 |       —      |  —  |   —  |
| Create time entry                 | `TimeEntries::TimeEntry::Create`                              |       —      |  —  |   —  |
| Create time entry kind            | `TimeEntryKinds::TimeEntryKind::Create`                       |       —      |  —  |   —  |
| Create time off kind              | `TimeOffKinds::TimeOffKind::Create`                           |       —      |  —  |   —  |
| Create time off                   | `TimeOffs::TimeOff::Create`                                   |       —      |  —  |   —  |
| Create workspace asset assignment | `WorkspaceAssetAssignments::WorkspaceAssetAssignment::Create` |       —      |  —  |   —  |
| Create workspace asset            | `WorkspaceAssets::WorkspaceAsset::Create`                     |       —      |  —  |   —  |
| End employment                    | `Employments::Employment::End`                                | `tools call` |  —  |   —  |
| Create benefit enrollment         | `BenefitEnrollments::BenefitEnrollment::Create`               |       ✓      |  ✓  |   ✓  |
| Create benefit                    | `Benefits::Benefit::Create`                                   |       ✓      |  ✓  |   ✓  |
| Create ccnl                       | `Ccnls::Ccnl::Create`                                         |       ✓      |  ✓  |   ✓  |
| Create compensation               | `Compensations::Compensation::Create`                         |       ✓      |  ✓  |   ✓  |
| Create employee                   | `Employees::Employee::Create`                                 |       ✓      |  ✓  |   ✓  |
| Create employment                 | `Employments::Employment::Create`                             |       ✓      |  ✓  |   ✓  |
| Create time clock                 | `TimeClocks::TimeClock::Create`                               |       ✓      |  ✓  |   ✓  |

## Reminders

0 of 1 covered.

| Action          | Key                           | Client | CLI | Demo |
| --------------- | ----------------------------- | :----: | :-: | :--: |
| Create reminder | `Reminders::Reminder::Create` |    —   |  —  |   —  |

## Reviews

0 of 3 covered.

| Action                 | Key                                       | Client | CLI | Demo |
| ---------------------- | ----------------------------------------- | :----: | :-: | :--: |
| Create review cycle    | `ReviewCycles::ReviewCycle::Create`       |    —   |  —  |   —  |
| Update review          | `Reviews::Review::Update`                 |    —   |  —  |   —  |
| Create review template | `ReviewTemplates::ReviewTemplate::Create` |    —   |  —  |   —  |

## Sales

1 of 3 covered.

| Action            | Key                             | Client | CLI | Demo |
| ----------------- | ------------------------------- | :----: | :-: | :--: |
| Create quote item | `QuoteItems::QuoteItem::Create` |    —   |  —  |   —  |
| Create quote      | `Quotes::Quote::Create`         |    —   |  —  |   —  |
| Create deal       | `Deals::Deal::Create`           |    ✓   |  ✓  |   ✓  |

## Stories

2 of 2 covered.

| Action       | Key                      | Client | CLI | Demo |
| ------------ | ------------------------ | :----: | :-: | :--: |
| Create story | `Stories::Story::Create` |    ✓   |  ✓  |   ✓  |
| Update story | `Stories::Story::Update` |    ✓   |  ✓  |   ✓  |

## Tasks

2 of 8 covered.

| Action               | Key                               | Client | CLI | Demo |
| -------------------- | --------------------------------- | :----: | :-: | :--: |
| Create cycle         | `Cycles::Cycle::Create`           |    —   |  —  |   —  |
| Update cycle         | `Cycles::Cycle::Update`           |    —   |  —  |   —  |
| Create initiative    | `Initiatives::Initiative::Create` |    —   |  —  |   —  |
| Update initiative    | `Initiatives::Initiative::Update` |    —   |  —  |   —  |
| Update task template | `Tasks::TaskTemplate::Update`     |    —   |  —  |   —  |
| Create task user     | `Tasks::TaskUser::Create`         |    —   |  —  |   —  |
| Create task          | `Tasks::Task::Create`             |    ✓   |  ✓  |   ✓  |
| Update task          | `Tasks::Task::Update`             |    ✓   |  ✓  |   ✓  |

## Workflows

0 of 6 covered.

| Action                     | Key                                     | Client | CLI | Demo |
| -------------------------- | --------------------------------------- | :----: | :-: | :--: |
| Update transform template  | `Transforms::TransformTemplate::Update` |    —   |  —  |   —  |
| Create workflow            | `Workflows::Workflow::Create`           |    —   |  —  |   —  |
| Create workflow step       | `Workflows::WorkflowStep::Create`       |    —   |  —  |   —  |
| Create workflow step item  | `Workflows::WorkflowStepItem::Create`   |    —   |  —  |   —  |
| Create workflow transition | `Workflows::WorkflowTransition::Create` |    —   |  —  |   —  |
| Create workflow trigger    | `Workflows::WorkflowTrigger::Create`    |    —   |  —  |   —  |

---

# GraphQL schema

URL: https://developers.uidu.org/docs/reference/schema

> Explore the uidu GraphQL schema interactively, and find the generated TypeScript types the SDK uses.

The SDK calls one GraphQL endpoint per workspace:

```
https://{workspace}.uidu.org/graphql
```

The schema is large — most of it is internal admin surface (invoices, expense reports, journal entries, approvals) that `@uidu/client` deliberately doesn't expose. The [Apps](https://developers.uidu.org/docs/domains/cms.md) section is the curated, public-facing slice.

## Explore the schema interactively

Open &#x2A;*[Apollo Sandbox](https://studio.apollographql.com/sandbox/explorer?endpoint=https%3A%2F%2Fuidu.uidu.org%2Fgraphql)** with the uidu endpoint pre-filled. You'll get autocomplete, a schema browser, query history, and shareable URLs — no install required.

To run authenticated queries, click the **Headers** tab in Sandbox and add:

```
X-Uidu-Public-Token: pk_live_...
```

(For your own workspace, swap `uidu.uidu.org` for `{your-workspace}.uidu.org` in the URL.)

<Callout type="info">
  Apollo Sandbox is hosted by Apollo Studio; nothing is sent to Apollo's servers except for the schema introspection round trip, which the workspace already serves publicly.
</Callout>

## Types in your editor

If you're writing TypeScript, the canonical reference is the generated types file in the SDK repo:

```
packages/client/src/generated/index.ts
```

Every type the SDK consumes — `Page`, `Event`, `Booking`, `Course`, every connection, every input — is exported from there. The file is regenerated against the live schema on every build of `@uidu/client`.

Import them directly when you need them:

```ts
import type {
  Page,
  Event,
  Booking,
  Course,
  Campaign,
  Contact,
  Deal,
  KbArticle,
} from '@uidu/client';
```

## Alternative explorers

* **GraphiQL** (`https://github.com/graphql/graphiql`) — self-host the React component if you want an explorer inside your own app.
* **Insomnia / Postman** — both have first-class GraphQL support with schema introspection. Useful for testing mutations with custom auth flows.
* **`graphql-request`** + a `.graphql` file — the [`@uidu/client`](https://developers.uidu.org/docs/foundations/installation.md) approach, ergonomic for production code.

## Why we don't ship 3000+ auto-generated reference pages

We tried (with [graphql-markdown](https://graphql-markdown.dev/)) and the output buried the curated docs under thousands of internal types. Apollo Sandbox is a better experience for the rare cases where you want to dig into the schema directly, and `generated/index.ts` is a better experience for type lookups while you write code.

---

# AI agents

URL: https://developers.uidu.org/docs/tools/ai-agents

> Set up Claude Code, Cursor, Codex or any coding agent to build on uidu — skills, the Claude Code plugin, Markdown docs and a CLI that speaks JSON.

Everything here is built to be read and driven by coding agents as well as people. Pick what your agent supports.

## Give it the docs

| URL                                        | What it is                                                                            |
| ------------------------------------------ | ------------------------------------------------------------------------------------- |
| [`/llms.txt`](/llms.txt)                   | An index of every page, with a one-line description each.                             |
| [`/llms-full.txt`](/llms-full.txt)         | The whole documentation in one Markdown file.                                         |
| `/docs/<page>.md`                          | Any page as Markdown — e.g. [`/docs/domains/cms.md`](https://developers.uidu.org/docs/domains/cms.md.md).           |
| [`/sdk-manifest.json`](/sdk-manifest.json) | Every export of the SDK with its domain, read/write, auth, signature and CLI command. |
| `/mcp`                                     | A remote MCP server over these docs — see [Docs MCP server](https://developers.uidu.org/docs/tools/docs-mcp.md).    |

Every page also has buttons under its title to copy its Markdown or open it in Claude or ChatGPT. A prompt that works well:

```text
Build a uidu-powered Next.js site. Read https://developers.uidu.org/llms.txt first,
then scaffold with `npm create uidu-app@latest`.
```

## Install the skills

Skills teach an agent uidu's canonical patterns: which package to use, how to fetch and render each domain, how to scaffold and provision. Install them with [skills.sh](https://skills.sh):

```bash
npx skills add uidu-org/api.js --skill uidu -a claude-code
```

Replace `claude-code` with your agent (`cursor`, `copilot`, `windsurf`, …).

| Skill         | For                                                                                                 |
| ------------- | --------------------------------------------------------------------------------------------------- |
| `uidu`        | The SDK: package map, env setup, fetching and rendering every domain, the CLI, scaffolding.         |
| `uidu-design` | How an app framed inside uidu should look: tokens, components, layout in the iframe, states, icons. |

## Claude Code plugin

The plugin bundles the `uidu` skill with a `/uidu-new-app` command that goes from login to a scaffolded, wired and provisioned app:

```text
/plugin marketplace add uidu-org/api.js
/plugin install uidu@uidu-org
```

## Let it drive the CLI

The [`uidu` CLI](https://developers.uidu.org/docs/tools/cli.md) is the agent's hands: every command takes `--json`, and `uidu login --password-grant` works without a browser for CI and headless agents.

```bash
uidu whoami --json
uidu events list --json
uidu events create --name "Spring gala" --json
```

<Callout type="warn" title="Keep write tokens out of the browser">
  Writes authenticate as your account with a Bearer token. Let the agent use it from the CLI or server-side code only — never in a client component.
</Callout>

---

# App bridge

URL: https://developers.uidu.org/docs/tools/app-bridge

> How a custom app talks to the uidu page that frames it — UiduAppProvider and useUiduApp, connect(), trusted origins, the protocol, and fromBridge.

The reference for the moving parts behind a [custom app](https://developers.uidu.org/docs/paths/custom-app.md). The path walks
through building one; this page is what each piece does.

* **`@uidu/app-bridge`** — the browser half of the handshake: `connect()` asks the uidu page framing
  the app for a session over `postMessage` and keeps its token fresh.
* **`@uidu/client`** — `fromBridge(bridge)` turns that session into a normal client config.
* **`@uidu/react`** — `<UiduAppProvider>` and `useUiduApp()` do both for a React tree.

## How the handshake works

```
uidu page (https://acme.uidu.org/…)                    your app (https://rooms.example.com)
  └─ <iframe src="https://rooms.example.com">
                                      ◀── uidu:ready      connect()
       uidu:context ──▶  { token, expiresAt, graphqlUrl, context }
                                      ◀── uidu:refresh    getToken(), before expiry
       uidu:context ──▶  (new token)

your app ── Authorization: Bearer <token> ──▶ graphqlUrl  (the workspace's own host)
```

The session lasts about five minutes and the bridge refreshes it on demand. `context` tells the
app who is looking and where: `user`, `space` (null when installed on the workspace),
`workspaceApp` (the installed app — the owner of its [Models](https://developers.uidu.org/docs/domains/models.md)), `locale`, `theme`
(`'light' | 'dark'`) and `accent` (the page's colour, to set as `--primary`).

***

## `<UiduAppProvider>` and `useUiduApp()` — `@uidu/react`

The React way in. `<UiduAppProvider>` connects on mount and renders its children in every
state; once connected it also wraps them in a [`<UiduProvider>`](https://developers.uidu.org/docs/reference/react.md) with a
session-backed client, so `useUiduClient()` works too. `useUiduApp()` returns the state.

```tsx
// app/layout.tsx renders <AppProvider> around {children}
'use client';
import { UiduAppProvider } from '@uidu/react';
import { DEFAULT_HOST_ORIGINS } from '@uidu/app-bridge';

export function AppProvider({ children }: { children: React.ReactNode }) {
  // https://*.uidu.org by default; add a local uidu or a custom domain
  return (
    <UiduAppProvider hostOrigins={[...DEFAULT_HOST_ORIGINS, 'https://me.uidu.local:8443']}>
      {children}
    </UiduAppProvider>
  );
}
```

```tsx
'use client';
import { useEffect, useState } from 'react';
import { useUiduApp } from '@uidu/react';
import { ensureModel, listModelItems, type ModelItem } from '@uidu/client';

export function Bookings() {
  const app = useUiduApp(); // status: 'connecting' | 'ready' | 'error'
  const [items, setItems] = useState<ModelItem[]>([]);

  const client = app.status === 'ready' ? app.client : null;
  const workspaceAppId = app.status === 'ready' ? app.context.workspaceApp.id : null;

  useEffect(() => {
    if (!client || !workspaceAppId) return;
    ensureModel(client, {
      workspaceAppId,
      name: 'Booking',
      fields: [{ shortname: 'room', name: 'Room', kind: 'string' }],
    })
      .then((model) => listModelItems(client, { modelId: model.id }))
      .then(setItems);
  }, [client, workspaceAppId]);

  if (app.status === 'connecting') return <p>Connecting…</p>;
  if (app.status === 'error') return <p>Open this app from uidu ({app.error.code})</p>;

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{String(item.fieldValuesByShortname?.room ?? '')}</li>
      ))}
    </ul>
  );
}
```

| Prop            | Default                | Description                                                                                           |
| --------------- | ---------------------- | ----------------------------------------------------------------------------------------------------- |
| `hostOrigins`   | `DEFAULT_HOST_ORIGINS` | Origins allowed to hand the app a session. **Replaces** the default — spread it when adding your own. |
| `timeout`       | `5000`                 | Milliseconds to wait for the host, per handshake or refresh.                                          |
| `refreshMargin` | `60000`                | Ask for a new token when the current one has less than this left.                                     |

**`useUiduApp()` returns** a `UiduAppState`:

| `status`       | Fields                                                                                                                                       |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `'connecting'` | everything else `null`                                                                                                                       |
| `'ready'`      | `bridge`, `client` (a `UiduClient`), `context` (kept up to date when the host sends a new one), `error` (the last failed refresh, or `null`) |
| `'error'`      | `error` — an `AppBridgeError`; there is no session                                                                                           |

`useUiduApp()` throws when called outside a `<UiduAppProvider>`.

***

## `connect(options)` — `@uidu/app-bridge`

Outside React — or to manage the bridge yourself — connect directly and build the client with
`fromBridge`.

```ts
import { connect, AppBridgeError, DEFAULT_HOST_ORIGINS } from '@uidu/app-bridge';
import { createClient, fromBridge, listGoals } from '@uidu/client';

try {
  const bridge = await connect({ hostOrigins: [...DEFAULT_HOST_ORIGINS] });
  const client = createClient(fromBridge(bridge));

  bridge.context.user?.name;
  bridge.context.workspaceApp.id;
  document.documentElement.classList.toggle('dark', bridge.context.theme === 'dark');

  bridge.on('context', (context) => {/* theme, locale… changed */});
  bridge.on('error', (error) => {/* a refresh was refused or timed out */});

  const goals = await listGoals(client);
} catch (error) {
  if (error instanceof AppBridgeError && error.code === 'NOT_EMBEDDED') {
    // opened directly, not inside uidu — fall back (localStorage, a notice…)
  }
}
```

`connect()` resolves once the host has sent the first session, and rejects with an
`AppBridgeError` otherwise:

| `code`            | Meaning                                                                                       |
| ----------------- | --------------------------------------------------------------------------------------------- |
| `NOT_EMBEDDED`    | Not inside an iframe (or not in a browser).                                                   |
| `UNKNOWN_HOST`    | Framed, but the browser didn't say by whom (no referrer).                                     |
| `UNTRUSTED_HOST`  | Framed by a page outside `hostOrigins`.                                                       |
| `TIMEOUT`         | The host didn't answer in time — usually a page without a bridge.                             |
| `HOST_ERROR`      | The host refused: not a member, app removed, …                                                |
| `INVALID_MESSAGE` | The host sent a session the bridge won't use (e.g. a `graphqlUrl` outside the host's origin). |
| `DESTROYED`       | `destroy()` was called.                                                                       |

**Returns:** `Promise<AppBridge>` — `context`, `graphqlUrl`, `hostOrigin`, `version`,
`getToken()` (cached; concurrent callers share one refresh), `on('context' | 'error', listener)`
(returns an unsubscribe function) and `destroy()`.

### Origins

Whoever frames the app could post it a fake session pointing at a `graphqlUrl` of its choosing,
so the bridge only trusts the origins you list:

* `DEFAULT_HOST_ORIGINS` — `['https://*.uidu.org']`.
* `matchesOrigin(origin, pattern)` — exact origin, or a wildcard where `*` stands for one or
  more subdomain labels (never the bare domain, scheme or port).
* `isTrustedOrigin(origin, patterns)` — any pattern matches.
* `detectParentOrigin(window)` — the framing page's origin, from `location.ancestorOrigins`
  or `document.referrer`.

Mirror the same list in your `Content-Security-Policy: frame-ancestors` header, so only uidu can
frame the app in the first place.

### Protocol

`PROTOCOL_VERSION` is `1`. The app posts `{ type: 'uidu:ready' }` on load and
`{ type: 'uidu:refresh' }` before the token expires; the host answers `uidu:context`
(`token`, `expiresAt`, `graphqlUrl`, `context`) or `uidu:error` (`message`).
`parseHostMessage(data)` narrows an inbound `event.data` to one of those, or `null` — it checks
the shape only, not the sender.

***

## `fromBridge(bridge)` — `@uidu/client`

Turns a connected bridge into a `UiduClientConfig`: `endpoint` is the session's `graphqlUrl`,
and `getToken` asks the bridge for a token on every request, so a refreshed token is picked up
without rebuilding the client. Typed structurally (`{ graphqlUrl, getToken }`), so
`@uidu/client` doesn't depend on the bridge.

```ts
import { createClient, fromBridge } from '@uidu/client';

const client = createClient(fromBridge(bridge));
```

**Returns:** `UiduClientConfig`. If the bridge can't produce a token, requests reject with a
`UiduError` whose code is `TOKEN_ERROR`.

***

## Reference

* [`@uidu/app-bridge` reference](https://developers.uidu.org/docs/reference/app-bridge.md) — every export of the bridge.
* [`@uidu/react` reference](https://developers.uidu.org/docs/reference/react.md) — `UiduAppProvider`, `useUiduApp` and types.

---

# Components

URL: https://developers.uidu.org/docs/tools/components

> uidu's design system as a shadcn registry — the theme and the components uidu itself uses, installed into your app with one command.

The registry at `https://developers.uidu.org/r` serves uidu's tokens and components, generated from uidu's own source. Install them with the [shadcn CLI](https://ui.shadcn.com/docs/cli): you get the files in your project, to read and change.

## Install everything

The `kit` installs the theme and every component, which is what an app framed inside uidu needs to look like the page around it:

```bash
npx shadcn@latest add https://developers.uidu.org/r/kit.json
```

## Add the registry once

Add the `@uidu` namespace to your `components.json`, then add components by name:

```json title="components.json"
{
  "registries": {
    "@uidu": "https://developers.uidu.org/r/{name}.json"
  }
}
```

```bash
npx shadcn@latest add @uidu/theme @uidu/button @uidu/field
```

The [custom-app template](/templates#custom-app) ships with the registry configured and the kit installed.

## What's in it

| Item                                                                                                                                                                                                                                                                                                                                                                                | Type                                                                                                                 |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `theme`                                                                                                                                                                                                                                                                                                                                                                             | uidu's tokens, light and dark: the neutral palette, the teal accent, success / warning / info and a `0.5rem` radius. |
| `kit`                                                                                                                                                                                                                                                                                                                                                                               | The theme plus every component below.                                                                                |
| `alert`, `alert-dialog`, `avatar`, `badge`, `button`, `button-group`, `calendar`, `card`, `checkbox`, `dialog`, `dropdown-menu`, `empty`, `field`, `input`, `input-group`, `item`, `kbd`, `label`, `popover`, `progress`, `radio-group`, `scroll-area`, `select`, `separator`, `sheet`, `skeleton`, `sonner`, `spinner`, `switch`, `table`, `tabs`, `textarea`, `toggle`, `tooltip` | Components                                                                                                           |

<Callout type="info">
  The registry is regenerated whenever uidu changes a token or one of these components, so pull updates with `npx shadcn@latest add @uidu/<name> --overwrite`.
</Callout>

## Data components

The components that render uidu data — `PageBlocks`, `BlockRenderer`, `RichText`, `DynamicForm` — ship in [`@uidu/react`](https://developers.uidu.org/docs/tools/react/components.md), not in the registry.

---

# create-uidu-app

URL: https://developers.uidu.org/docs/tools/create-uidu-app

> Scaffold a new uidu-powered Next.js app from a template.

```bash
npm create uidu-app@latest my-app
npm create uidu-app@latest my-app -- --template events
```

## Templates

| Template     | What it builds                                                                                                                                    |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `minimal`    | Landing page from a legacy CMS Project, rendered with `<PageBlocks>` (for a Site, follow the [Public website](https://developers.uidu.org/docs/paths/public-website.md) path)   |
| `events`     | Events listing + `/event/[id]` detail (`listEvents` / `getEvent`)                                                                                 |
| `stories`    | Blog listing + `/story/[id]` detail (`listStories` / `getStory`)                                                                                  |
| `donations`  | Campaigns listing + `/campaign/[id]` detail with progress bars                                                                                    |
| `custom-app` | An app uidu shows inside a Space: `<UiduAppProvider>`, data in its own Models (`public/uidu.app.json`) — see [Custom app](https://developers.uidu.org/docs/paths/custom-app.md) |

Every template ships Next.js 16 App Router, React 19, Tailwind v4 and an `.env.example`. The site
templates add a configured `src/lib/uidu.ts`; `custom-app` has no token to configure.

## Options

| Flag                     | Description                                    |
| ------------------------ | ---------------------------------------------- |
| `-t, --template <scope>` | Template (default `minimal`)                   |
| `-f, --framework <id>`   | Framework (default `next`, the only one today) |
| `--package-manager <pm>` | Override the detected package manager          |
| `--no-install`           | Skip installing dependencies                   |
| `--no-git`               | Skip `git init`                                |
| `-y, --yes`              | Accept all defaults (non-interactive)          |

The [`uidu` CLI](https://developers.uidu.org/docs/tools/cli.md) wraps it: `uidu create my-app -t events`.

---

# Docs MCP server

URL: https://developers.uidu.org/docs/tools/docs-mcp

> Give an AI assistant these docs as tools — search them, read a page, look up any SDK function — through a remote MCP server.

`https://developers.uidu.org/mcp` is a read-only [MCP](https://modelcontextprotocol.io) server over this
documentation. Add it to Claude, ChatGPT, Cursor or any MCP client and the assistant can look things
up while it writes code against the SDK — no install, no token, always matching the published docs.

## Tools

| Tool                         | What it returns                                                                                                                                                                                    |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `search_docs(query, limit?)` | The best-matching pages, with snippets and each page's Markdown URL. Guides rank above the generated reference.                                                                                    |
| `get_page(path)`             | One page as Markdown — `"domains/forms"`, `"/docs/paths/custom-app"` or a full docs URL.                                                                                                           |
| `get_function(name)`         | One export of `@uidu/client`, `@uidu/react` or `@uidu/app-bridge`: signature, description, read/write, auth, CLI command, domain guide, source. Suggests close names when there is no exact match. |
| `list_domains()`             | Every domain the client covers, with what its reads need, its guide, CLI commands and functions.                                                                                                   |

The server's instructions tell the assistant where to start (the three [paths](https://developers.uidu.org/docs/paths/public-website.md)) and the auth rule of thumb from [Authentication](https://developers.uidu.org/docs/foundations/authentication.md).

## Connect

**Claude Code**

```bash
claude mcp add --transport http uidu-docs https://developers.uidu.org/mcp
```

**Claude (web and desktop)** — *Settings → Connectors → Add custom connector*, with the URL
`https://developers.uidu.org/mcp`.

**Cursor** — in `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "uidu-docs": { "url": "https://developers.uidu.org/mcp" }
  }
}
```

**A stdio-only client** — bridge it with `mcp-remote`:

```json
{
  "mcpServers": {
    "uidu-docs": { "command": "npx", "args": ["-y", "mcp-remote", "https://developers.uidu.org/mcp"] }
  }
}
```

## Without MCP

The same content is published for agents as plain files: [`/llms.txt`](/llms.txt) (the index),
[`/llms-full.txt`](/llms-full.txt) (every page) and [`/sdk-manifest.json`](/sdk-manifest.json) (every
export). Every page also has a Markdown twin at its URL plus `.md`.

<Callout title="Docs, not your workspace">
  This server only reads the documentation. To read or change a workspace from an agent, use the
  [`uidu` CLI](https://developers.uidu.org/docs/tools/cli.md) — or `@uidu/client` in your own code.
</Callout>

---

# Commands

URL: https://developers.uidu.org/docs/tools/cli/commands

> The uidu CLI command reference — entities (list/get/create), CMS, provisioning, and scaffolding.

All commands accept `--json` for machine-readable output (what AI agents parse), `--workspace`
to override the workspace, and `--first <n>` for list page size.

## Entities

Most scopes are exposed through a uniform verb set:

```bash
uidu <entity> list [--first 50]
uidu <entity> get <id>
uidu <entity> create --attributes '<json>'        # write — needs a Bearer token
uidu <entity> update <id> --attributes '<json>'   # write
uidu <entity> delete <id>                          # write
```

| Entity                | list | get | create | update | delete |
| --------------------- | :--: | :-: | :----: | :----: | :----: |
| `events`              |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
| `attendances`         |   —  |  —  |    ✓   |    —   |    —   |
| `stories`             |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
| `donations`           |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `courses`             |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `forms`               |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `contacts`            |   ✓  |  ✓  |    ✓   |    —   |    ✓   |
| `deals`               |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
| `goals`               |   ✓  |  ✓  |    —   |    ✓   |    —   |
| `timeframes`          |   ✓  |  —  |    —   |    —   |    —   |
| `employees`           |   ✓  |  —  |    ✓   |    ✓   |    —   |
| `employments`         |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `offices`             |   ✓  |  —  |    —   |    —   |    —   |
| `roles`               |   ✓  |  —  |    —   |    —   |    —   |
| `ccnls`               |   ✓  |  —  |    ✓   |    —   |    —   |
| `bookings`            |   ✓  |  ✓  |    —   |    —   |    —   |
| `calls`               |   ✓  |  ✓  |    —   |    —   |    —   |
| `jobs`                |   ✓  |  ✓  |    —   |    —   |    —   |
| `applications`        |   ✓  |  —  |    ✓   |    —   |    —   |
| `campaigns`           |   ✓  |  ✓  |    —   |    —   |    —   |
| `kb-collections`      |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `kb-articles`         |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `channel`             |   ✓  |  ✓  |    ✓   |    ✓   |    ✓   |
| `time-clocks`         |   —  |  —  |    ✓   |    —   |    —   |
| `compensations`       |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `tasks`               |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `benefits`            |   —  |  —  |    ✓   |    —   |    —   |
| `benefit-enrollments` |   —  |  —  |    ✓   |    —   |    —   |
| `notes`               |   —  |  —  |    ✓   |    ✓   |    ✓   |
| `projects`            |   ✓  |  —  |    —   |    —   |    —   |
| `sites`               |   —  |  —  |    ✓   |    —   |    —   |
| `spaces`              |   —  |  —  |    ✓   |    ✓   |    ✓   |

```bash
uidu events list --first 20 --json
uidu stories get Z2lkOi8v… --json
uidu events create --name "Launch party" --json
uidu channel update Z2lkOi8v… --attributes '{"name":"Help center"}' --json
uidu tasks delete Z2lkOi8v… --json
```

<Callout type="info" title="Capability varies by entity">
  The table reflects what the backend exposes. A verb the entity doesn't support fails with
  `<entity> does not support <verb>` — see the [authoring reference](https://developers.uidu.org/docs/foundations/writes.md)
  for the write model.
</Callout>

## All other commands

Every command outside the entity registry, generated from the CLI's own `--help`:

| Command                      | What it does                                                     |
| ---------------------------- | ---------------------------------------------------------------- |
| `uidu pages list`            | List CMS pages                                                   |
| `uidu page get <slug>`       | Fetch one CMS page                                               |
| `uidu page create`           | Create a page (--name --slug \| --attributes \<json>)            |
| `uidu block create`          | Create a page block (--attributes \<json>)                       |
| `uidu field create`          | Create a field value (--attributes \<json>)                      |
| `uidu workspace create`      | Create a workspace (--name \| --attributes \<json>)              |
| `uidu workspace credentials` | Generate workspace API credentials                               |
| `uidu project create`        | Create a project (--attributes \<json>)                          |
| `uidu tools call <key>`      | Run one action (--attributes \<json>, associations as GlobalIDs) |

## CMS

Pages are project-scoped — pass `--project <id>` or set `UIDU_PROJECT_ID`.

```bash
uidu pages list --project proj_xyz --json
uidu page get home --project proj_xyz --json
uidu page create --name Home --slug home --attributes '{"projectId":"proj_xyz"}'
uidu block create --attributes '<json>'
uidu field create --attributes '<json>'
```

## Provisioning

```bash
uidu workspace create --name "Acme"
uidu workspace credentials          # rotate/return the workspace api key + secret
uidu project create --attributes '<json>'
uidu spaces create --name "Team"
uidu tasks create --name "Do the thing"
uidu notes create --attributes '{"body":"…"}'
```

<Callout type="info" title="Attributes shape">
  `create` commands send the mutation's `attributes` input. Use `--name` / `--slug` for common
  fields, or `--attributes '<json>'` for the full object. The command returns the mutation payload
  including an `errors` array — always non-empty on validation failure (the CLI exits non-zero).
</Callout>

## Scaffold

```bash
uidu create my-app                  # interactive
uidu create my-app -t events        # pick a template (delegates to create-uidu-app)
```

## Composed example — zero to site

```bash
uidu login --workspace acme
uidu workspace credentials --json          # capture publicToken for the app
uidu page create --name Home --slug home --attributes '{"projectId":"proj_xyz"}'
uidu create acme-site -t minimal           # scaffold a Next.js app wired to the workspace
```

---

# Overview

URL: https://developers.uidu.org/docs/tools/cli

> The uidu CLI — read a workspace, scaffold an app, and provision content from your terminal or an AI coding agent.

`@uidu/cli` (binary: `uidu`) wraps [`@uidu/client`](https://developers.uidu.org/docs/paths/public-website.md) in a
command-line tool. It's built to be driven by **humans and AI coding agents** (e.g. Claude
Code) alike: read a workspace, scaffold a new project, and provision content — all with
machine-readable `--json` output.

```bash
uidu login                         # browser sign-in (OAuth + PKCE)
uidu stories list --json           # read
uidu events create --name "Launch" # write (needs a Bearer token)
uidu create my-app -t events       # scaffold
```

## Reads vs writes — where each runs

The CLI and the client share one rule, because they share one engine:

| Operation                  | Auth                     | Where it can run                            |
| -------------------------- | ------------------------ | ------------------------------------------- |
| **Reads** (`list` / `get`) | public token             | anywhere — CLI, server, **and the browser** |
| **Writes** (`create`)      | account **Bearer** token | CLI and **server-side** frontend only       |

<Callout type="warn" title="Never ship the account token to the browser">
  Authoring (`create…`) authenticates as your account via a Bearer token. Keep it server-side —
  the CLI (`uidu login`), a Next.js server action, a route handler, or an RSC. The same write
  functions live in `@uidu/client`, so anything you can do from the CLI you can do from your
  backend; just never expose the Bearer token in a client component.
</Callout>

## Install

The CLI ships with the SDK. Run it with your package manager's `dlx`/`npx`, or install it
globally:

```bash
npx @uidu/cli --help
# or
npm install -g @uidu/cli
```

## Configuration

Every command resolves connection + auth in this order: &#x2A;*CLI flags → environment
(`UIDU_*`) → `~/.uidu/config.json`** (written by `uidu login`).

| Env                 | Purpose                                          |
| ------------------- | ------------------------------------------------ |
| `UIDU_WORKSPACE`    | Workspace slug (required)                        |
| `UIDU_PUBLIC_TOKEN` | Read-only token (safe to expose)                 |
| `UIDU_API_KEY`      | Account Bearer token for authoring (server-only) |
| `UIDU_PROJECT_ID`   | Default project for CMS reads                    |
| `UIDU_ENDPOINT`     | Override the GraphQL endpoint                    |

Continue with [Login →](https://developers.uidu.org/docs/tools/cli/login.md) or the [Command reference →](https://developers.uidu.org/docs/tools/cli/commands.md).

---

# Login

URL: https://developers.uidu.org/docs/tools/cli/login

> Authenticate the uidu CLI via the OAuth browser flow (Authorization Code + PKCE), with a password-grant fallback for CI and AI agents.

`uidu login` obtains an account **Bearer token** (needed for authoring) and stores it in
`~/.uidu/config.json`. Two flows are supported.

## Browser flow (default) — Authorization Code + PKCE

```bash
uidu login --workspace your-workspace
```

This opens your browser to sign in (the standard native-app flow, like `gh` or `vercel`):

1. the CLI starts a loopback server on `http://localhost:4123/callback` (override with `--port`);
2. your browser signs in against the uidu identity provider;
3. it redirects back to the loopback with an authorization `code`;
4. the CLI exchanges the code (with a PKCE verifier — no client secret) for a token.

<Callout type="info" title="localhost is your machine">
  The `localhost` redirect is the CLI listening on your own computer — the auth server only tells
  the browser where to go. This is RFC 8252 (OAuth for Native Apps); the same shared public client
  is used for everyone.
</Callout>

On a remote/SSH session (no browser), add `--no-browser` to print the URL instead of opening it
(the loopback still receives the redirect).

## Non-interactive (CI / AI agents)

When there's no browser, use the password grant:

```bash
UIDU_PASSWORD=… uidu login --email you@uidu.org --password-grant
```

Credentials come from `--email` + `UIDU_PASSWORD` (the password is never a flag). This is the
path AI agents and CI pipelines use.

## `whoami` / `logout`

```bash
uidu whoami --json   # workspace, account, token presence + expiry
uidu logout          # clears ~/.uidu/config.json
```

<Callout type="warn" title="Tokens are short-lived">
  Access tokens currently expire after \~2h and there is no refresh token — re-run `uidu login`
  when `whoami` reports `tokenExpired: true`.
</Callout>

## Self-hosting note

`login` targets the shared uidu identity provider by default. Self-hosted deployments need a
public OAuth client registered on their provider (with the `http://localhost:4123/callback`
redirect) and PKCE enabled; point the CLI at it with `--client-id` / `UIDU_CLIENT_ID` and
`--oauth-base` / `UIDU_OAUTH_BASE`.

---

# Components

URL: https://developers.uidu.org/docs/tools/react/components

> Prebuilt React components for rendering uidu CMS content.

## `<PageBlocks pageBlocks components fallback? />`

Walks a page's `pageBlocks` array and renders each block using the component registered for its shortname. Returns null when `pageBlocks` is empty.

```tsx
import { getPage } from '@uidu/client';
import { PageBlocks } from '@uidu/react';
import { uidu, config } from '@/lib/uidu';
import { Hero, Feature, ContactForm } from '@/components/blocks';

export default async function HomePage() {
  const page = await getPage(uidu, {
    projectId: config.projectId,
    slug: 'home',
  });
  if (!page) return null;

  return (
    <PageBlocks
      pageBlocks={page.pageBlocks}
      components={{
        Header: Hero,
        feature1: Feature,
        ModuloContatto: ContactForm,
      }}
    />
  );
}
```

### How block components receive props

Each component receives the &#x2A;*raw `PageBlock`** as `props.block`:

```tsx
import type { Page } from '@uidu/client';
import { useFields, toText } from '@uidu/react';

type PageBlock = NonNullable<Page['pageBlocks']>[number];

export function Hero({ block }: { block: PageBlock }) {
  const fields = useFields(block.fieldValues);
  return <h1>{toText(fields.Titolo)}</h1>;
}
```

The block's shortname is derived from `block.templateBlock.block.shortname` first (the canonical block type), falling back to `block.shortname`.

### Handling unknown blocks

Pass a `fallback` to render a placeholder for blocks whose shortname isn't in your map:

```tsx
<PageBlocks
  pageBlocks={page.pageBlocks}
  components={blockComponents}
  fallback={({ shortname }) => (
    <div className="rounded border border-dashed p-3 text-xs">
      Unknown block: <code>{shortname}</code>
    </div>
  )}
/>
```

Without a fallback, unknown blocks are silently skipped.

## `<BlockRenderer pageBlocks shortname components />`

Render exactly one block by its shortname. Useful when blocks need to be placed in specific spots in your layout rather than rendered sequentially.

```tsx
<header>
  <BlockRenderer
    pageBlocks={page.pageBlocks}
    shortname="Header"
    components={{ Header: Hero }}
  />
</header>

<main>
  {/* other content */}
</main>

<aside>
  <BlockRenderer
    pageBlocks={page.pageBlocks}
    shortname="Sidebar"
    components={{ Sidebar: SidebarBlock }}
  />
</aside>
```

If no block matches the shortname, returns null.

## `<RichText doc />`

Render a Tiptap/ProseMirror JSON document as React elements. Used by uidu CMS rich-text fields (`Event.body`, `Call.body`, page block rich-text fields, etc.).

```tsx
import { getEvent } from '@uidu/client';
import { RichText } from '@uidu/react';
import { uidu } from '@/lib/uidu';

export default async function EventPage({ params }) {
  const event = await getEvent(uidu, { id: params.id });
  return (
    <article>
      <h1>{event?.name}</h1>
      <RichText doc={event?.body} />
    </article>
  );
}
```

### Supported nodes

| Tiptap type                  | Output                                     |
| ---------------------------- | ------------------------------------------ |
| `doc`                        | wrapping `<div>`                           |
| `paragraph`                  | `<p>`                                      |
| `heading` (level 1–6)        | `<h1>`–`<h6>`, scaled type                 |
| `bulletList` / `orderedList` | `<ul>` / `<ol>`                            |
| `listItem`                   | `<li>`                                     |
| `blockquote`                 | `<blockquote>` with left border            |
| `codeBlock`                  | `<pre><code>` with dark background         |
| `image`                      | `<img>` (`src`, `alt`, `title` from attrs) |
| `horizontalRule`             | `<hr>`                                     |
| `hardBreak`                  | `<br>`                                     |
| `text`                       | bare text node, marks wrap inside-out      |

### Supported text marks

| Tiptap mark                | Output                             |
| -------------------------- | ---------------------------------- |
| `bold` / `strong`          | `<strong>`                         |
| `italic` / `em`            | `<em>`                             |
| `underline`                | `<u>`                              |
| `strike` / `strikethrough` | `<s>`                              |
| `code`                     | inline `<code>` with pill styling  |
| `link`                     | `<a>` with `href`, `target`, `rel` |

### Graceful degradation

Unknown node types render their children inside a plain `<div>` — content survives even if your CMS uses custom nodes the renderer doesn't recognize.

If `doc` is a string (some CMSes store rendered HTML), it falls back to `dangerouslySetInnerHTML`. If `doc` is null, undefined, or not an object, the component renders nothing.

### Types

```ts
import type { RichTextNode, RichTextProps } from '@uidu/react';

// RichTextNode — the recursive Tiptap node shape: { type, content?, text?, marks?, attrs? }
```

***

## `<DynamicForm form action />`

Render any uidu `Form` schema as inputs, collect the values, and call your Server Action. Built on `useTransition`, no SWR or `react-hook-form` dependency. Used in the demo for contact forms, RSVPs, job applications, donations, and standalone form submissions — same component, different `action`.

```tsx
'use client';
import { DynamicForm } from '@uidu/react';
import { submitContactForm } from '@/lib/actions';

export function ContactForm({ form }) {
  return (
    <DynamicForm
      form={form}
      action={submitContactForm}
      submitLabel="Send message"
      renderSuccess={(result) => (
        <p>Thanks! Reference: <code>{String(result.meta?.responseId)}</code></p>
      )}
    />
  );
}
```

### Props

| Prop                   | Type                                                        | Default           | Description                                                                                     |
| ---------------------- | ----------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------- |
| `form`                 | `Form`                                                      | —                 | The schema from `getForm`, `call.form`, or an embedded page block.                              |
| `action`               | `(values: DynamicFormValues) => Promise<DynamicFormResult>` | —                 | Server Action wrapping `createFormResponse`. Receives parsed values, returns `{ ok, errors? }`. |
| `includeContactFields` | `boolean`                                                   | `true`            | Render first name, last name, and email above the form questions.                               |
| `submitLabel`          | `string`                                                    | `"Submit"`        | Submit button label.                                                                            |
| `submittingLabel`      | `string`                                                    | `"Submitting…"`   | Submit button label while the action is pending.                                                |
| `renderSuccess`        | `(result) => ReactNode`                                     | default green box | Custom success state.                                                                           |
| `classNames`           | `{ form?, input?, button?, … }`                             | sensible Tailwind | Per-element class overrides.                                                                    |

### Input type detection

The component inspects each `FormQuestion`'s `fieldKind` and `field.options` to pick the right HTML control:

| Signal                                               | Renders                   |
| ---------------------------------------------------- | ------------------------- |
| `field.options` is non-empty                         | `<select>`                |
| `fieldKind: 'checkbox'` or `'boolean'`               | `<input type="checkbox">` |
| `fieldKind: 'textarea'`, `'longtext'`, `'multiline'` | `<textarea>`              |
| `fieldKind: 'email'`                                 | `<input type="email">`    |
| `fieldKind: 'number'`, `'integer'`                   | `<input type="number">`   |
| `fieldKind: 'date'`                                  | `<input type="date">`     |
| `fieldKind: 'tel'`, `'phone'`                        | `<input type="tel">`      |
| `fieldKind: 'url'`                                   | `<input type="url">`      |
| anything else                                        | `<input type="text">`     |

Hidden questions (`hidden: true`) and system fields (`field.type === 'system'`) are filtered out automatically — the latter are handled by `includeContactFields`.

### The action contract

Your `action` receives parsed `DynamicFormValues`:

```ts
{
  contact?: { firstName, lastName, email },         // only if includeContactFields
  fieldValues: [{ fieldId, questionId, value }, …], // one per non-system question
  formData: FormData,                                // raw, in case you need extras
}
```

A typical `submitForm` Server Action looks like:

```ts
'use server';
import { createFormResponse } from '@uidu/client';
import { uidu } from './uidu';

export async function submitForm(formId, values) {
  const result = await createFormResponse(uidu, {
    input: {
      attributes: {
        formId,
        fieldValuesAttributes: values.fieldValues.map((fv) => ({
          fieldId: fv.fieldId,
          content: { value: fv.value },
        })),
        contactAttributes: values.contact && {
          email: values.contact.email,
          contactableAttributes: {
            kind: 'person',
            firstName: values.contact.firstName,
            lastName: values.contact.lastName || undefined,
          },
        },
      },
      event: 'complete!',
    },
  });
  return result?.errors?.length
    ? { ok: false as const, errors: result.errors }
    : { ok: true as const, meta: { responseId: result?.formResponse?.id } };
}
```

The same action shape works for contact forms, RSVPs (with a different mutation), donations (different mutation again), and job applications.

### Styling

Defaults are reasonable Tailwind classes. Override per element with the `classNames` prop:

```tsx
<DynamicForm
  form={form}
  action={action}
  classNames={{
    input: 'rounded-xl border-emerald-200 focus:border-emerald-500',
    button: 'rounded-xl bg-emerald-600 hover:bg-emerald-700',
  }}
/>
```

For deeper customization (custom Input components, field-level adornments, complex layout), drop down to rendering the questions yourself — `form.inputs.edges` gives you everything `<DynamicForm>` uses internally.

***

## Helper exports

```ts
import { getBlockShortname, type BlockComponent } from '@uidu/react';

// getBlockShortname(block) — extracts the canonical shortname from a PageBlock.
//   Tries block.templateBlock.block.shortname first, then block.shortname.
//
// BlockComponent — type alias for ComponentType<{ block: PageBlock }>.
//   Use it to type your own block component registries.
```

## Patterns

### Per-route component maps

Different pages can use different block components. Keep the registry close to the page:

```tsx
// app/blog/[slug]/page.tsx
const blogBlocks = {
  Header: BlogHero,
  Body: ProseBody,
  CTA: NewsletterSignup,
};

export default async function BlogPost({ params }) {
  const page = await getPage(uidu, { projectId, slug: params.slug });
  return <PageBlocks pageBlocks={page?.pageBlocks} components={blogBlocks} />;
}
```

### Shared registry across pages

For consistency, hoist the registry into `lib/`:

```ts
// lib/blocks.ts
export const siteBlocks = {
  Header: Hero,
  feature1: Feature,
  ModuloContatto: ContactForm,
  /* ... */
};
```

```tsx
// any page
import { siteBlocks } from '@/lib/blocks';
<PageBlocks pageBlocks={page.pageBlocks} components={siteBlocks} />
```

---

# Getting Started

URL: https://developers.uidu.org/docs/tools/react/getting-started

> React provider and conventions for using @uidu/client in React applications.

`@uidu/react` is a thin React layer over [`@uidu/client`](https://developers.uidu.org/docs/paths/public-website.md). It exposes a provider so Client Components can reach the client, a small set of pure-transform hooks for normalizing CMS data, and components for rendering dynamic CMS pages.

<Callout type="info" title="Server-first by default">
  Most uidu data fetching happens in **Server Components** using `@uidu/client` directly — no provider needed. Reach for `@uidu/react` only when you need interactivity on the client (form submission, search-as-you-type, etc.) or want the prebuilt renderers.
</Callout>

## What's inside

| Area                                       | Exports                                   |
| ------------------------------------------ | ----------------------------------------- |
| [Provider](#provider)                      | `UiduProvider`                            |
| [Hooks](https://developers.uidu.org/docs/tools/react/hooks.md)           | `useUiduClient`, `useUidu`, `useFields`   |
| [Components](https://developers.uidu.org/docs/tools/react/components.md) | `PageBlocks`, `BlockRenderer`, `RichText` |
| Utilities                                  | `toText`, `getBlockShortname`             |

## Install

```bash
npm install @uidu/react @uidu/client
```

`@uidu/client` is a peer-style runtime dependency — install both.

## Provider

`UiduProvider` is the only piece of `@uidu/react` that needs to be wired into your app. Wrap any subtree that contains Client Components needing access to the uidu client.

```tsx
// app/layout.tsx
import { UiduProvider } from '@uidu/react';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <UiduProvider
          workspace={process.env.NEXT_PUBLIC_UIDU_WORKSPACE!}
          publicToken={process.env.NEXT_PUBLIC_UIDU_PUBLIC_TOKEN!}
        >
          {children}
        </UiduProvider>
      </body>
    </html>
  );
}
```

`UiduProvider` accepts the same options as [`createClient`](https://developers.uidu.org/docs/paths/public-website.md#create-a-client), minus `apiKey` (mutations should go through Server Actions, not the browser client). You can also pass an existing client instance:

```tsx
import { createClient } from '@uidu/client';
import { UiduProvider } from '@uidu/react';

const client = createClient({ workspace: 'my-org', publicToken: 'pk_...' });

<UiduProvider client={client}>{children}</UiduProvider>
```

The provider also wires SWR with a fetcher bound to the client, so [`useQuery`](https://developers.uidu.org/docs/tools/react/hooks.md#usequerydocument-variables-options) and any SWR hook of your own use the same client and cache.

## Two clients, by design

Most uidu apps end up with **two client instances**:

1. **Server client** — created in `lib/uidu.ts` with `createClient()` including the API key. Used in Server Components for data fetching and from Server Actions for mutations.
2. **Browser client** — created inside `UiduProvider` from the `workspace` + `publicToken` props. Reachable from Client Components via [`useUiduClient`](https://developers.uidu.org/docs/tools/react/hooks.md#useuiduclient). Never has the API key.

Don't try to share a single client between server and browser — the API key would leak.

## What's next

* [Hooks](https://developers.uidu.org/docs/tools/react/hooks.md) — `useUiduClient`, `useFields`, and friends
* [Components](https://developers.uidu.org/docs/tools/react/components.md) — `PageBlocks`, `BlockRenderer`, `RichText`
* [Demo app](https://developers.uidu.org/docs/recipes/demo-app.md) — a Next.js project exercising everything in one place

---

# Hooks

URL: https://developers.uidu.org/docs/tools/react/hooks

> React hooks for fetching uidu data on the client, accessing the client, and normalizing CMS data.

`useQuery` fetches data in Client Components. `useUiduClient` and `useUidu` read the client from context. `useFields` and `toText` are pure transforms for CMS field values, safe on the server too. Custom apps framed inside uidu also get `useUiduApp`, covered in [Custom apps](https://developers.uidu.org/docs/paths/custom-app.md). The [`@uidu/react` reference](https://developers.uidu.org/docs/reference/react.md) lists every export.

In Server Components you don't need any of these: `await` a `@uidu/client` function directly.

## `useQuery(document, variables?, options?)`

Run any generated `@uidu/client` document from a Client Component. It uses SWR over the client from the nearest [`UiduProvider`](https://developers.uidu.org/docs/tools/react/getting-started.md#provider): results are cached and deduplicated by operation name + variables, and revalidate on focus by default.

```tsx
'use client';
import { useQuery } from '@uidu/react';
import { ListEventsDocument } from '@uidu/client';

export function UpcomingEvents() {
  const { data, error, isLoading } = useQuery(ListEventsDocument, { first: 10 });

  if (isLoading) return <p>Loading…</p>;
  if (error) return <p>Couldn't load events.</p>;

  const events = data?.currentWorkspace?.events?.edges?.map((e) => e?.node) ?? [];
  return <ul>{events.map((ev) => ev && <li key={ev.id}>{ev.name}</li>)}</ul>;
}
```

|               |                                                                                                                                                                         |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Arguments** | `document` — any `*Document` exported by `@uidu/client`; `variables` — its typed variables; `options` — `revalidateOnFocus`, `revalidateOnReconnect`, `refreshInterval` |
| **Returns**   | `{ data, error, isLoading, mutate }` — `data` is the raw query result (the GraphQL shape, not the flattened array a `list*` function returns)                           |
| **Runtime**   | Client only — requires `UiduProvider`                                                                                                                                   |

<Callout type="info" title="Raw shape">
  `useQuery` returns what the query returns, so connections come as `edges[].node`. The `list*` functions in `@uidu/client` flatten that for you; with `useQuery` you do it yourself, as above.
</Callout>

There are no per-domain hooks (`useEvents`, `usePage`…) yet: `useQuery` plus a generated document covers the same ground.

## `useUiduClient()`

Read the client instance from any Client Component descendant of [`UiduProvider`](https://developers.uidu.org/docs/tools/react/getting-started.md#provider).

```tsx
'use client';
import { useUiduClient } from '@uidu/react';
import { search } from '@uidu/client';

export function SearchButton({ q }: { q: string }) {
  const client = useUiduClient();

  async function run() {
    // any @uidu/client function works with this client
    const results = await search(client, {
      params: { filters: [{ field: 'name', op: 'matches', value: q }] },
    });
    console.log(results);
  }

  return <button onClick={run}>Search</button>;
}
```

|             |                                                                     |
| ----------- | ------------------------------------------------------------------- |
| **Returns** | `UiduClient` instance                                               |
| **Throws**  | `Error` with a descriptive message if called outside `UiduProvider` |
| **Runtime** | Client only — requires React context                                |

<Callout type="warn" title="Server Components">
  Don't call `useUiduClient()` from a Server Component. Server Components should import the server-side client directly from `lib/uidu.ts` (the one with the API key).
</Callout>

## `useUidu()`

Lower-level accessor — returns the full provider context: the client plus the resolved endpoint URL. Rarely needed in app code; prefer `useUiduClient()` unless you specifically need the endpoint.

```tsx
'use client';
import { useUidu } from '@uidu/react';

export function DebugInfo() {
  const { client, endpoint } = useUidu();
  return <code>{endpoint}</code>;
}
```

|             |                                            |
| ----------- | ------------------------------------------ |
| **Returns** | `{ client: UiduClient, endpoint: string }` |
| **Throws**  | `Error` if called outside `UiduProvider`   |

## `useFields(fieldValues)`

Normalize a `fieldValues` array (from a CMS page block) into a `{ shortname: value }` map. **Pure function** — safe to call from Server Components, Client Components, or anywhere else. The `use` prefix is a naming convention only; no React state or effects are involved.

```tsx
import type { Page } from '@uidu/client';
import { useFields, toText } from '@uidu/react';

type PageBlock = NonNullable<Page['pageBlocks']>[number];

export function Hero({ block }: { block: PageBlock }) {
  const fields = useFields(block.fieldValues);
  return (
    <header>
      <h1>{toText(fields.Titolo)}</h1>
      <p>{toText(fields.Sottotitolo)}</p>
    </header>
  );
}
```

### Three kinds of values

`useFields` handles the three shapes a uidu field value can take:

| Field `kind`                         | Output                                                                         |
| ------------------------------------ | ------------------------------------------------------------------------------ |
| Scalar (text, number, boolean, etc.) | `fieldValue.content.value` — the raw value                                     |
| Collection                           | `fieldValue.content` — the whole content object (typically `{ items: [...] }`) |
| Linked record                        | `fieldValue.linkedRecord` — the embedded record (e.g. a Form, a Contact)       |

Field values without a `shortname` are dropped.

|             |                                 |
| ----------- | ------------------------------- |
| **Returns** | `Record<string, unknown>`       |
| **Pure**    | Yes — no React state or effects |
| **Runtime** | Anywhere                        |

## `toText(value)` (utility)

Flatten any field value to a plain string. Handles raw strings, numbers, arrays of nodes, and Slate-style rich text trees (`{ type, children }`, `{ text }`). Used in tandem with `useFields` to handle the case where the CMS stores a "title" field as either a plain string **or** a rich-text fragment.

```tsx
toText('hello');                                          // → 'hello'
toText(42);                                               // → '42'
toText({ type: 'paragraph', children: [{ text: 'hi' }] }); // → 'hi'
toText([{ text: 'a ' }, { text: 'b', bold: true }]);      // → 'a b'
toText(null);                                             // → ''
```

For genuine rich text (not just a single string), use [`<RichText>`](https://developers.uidu.org/docs/tools/react/components.md#richtext-doc-) — it renders the same tree as React elements with proper marks, lists, headings, and so on.

|             |                                                                |
| ----------- | -------------------------------------------------------------- |
| **Returns** | `string` (empty string when value is null/undefined/unhandled) |
| **Pure**    | Yes                                                            |
| **Runtime** | Anywhere                                                       |
