uidu SDK
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.

// 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.

Read private data

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

// 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,
  });
}

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.

Fields, filters and helpers: Contacts & deals (amount is in cents), People, Goals (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:

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.

Write, and handle errors

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

'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; error codes and what to retry: Errors.

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.

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; sign-in flows and token expiry: Login.

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).

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. Field names per entity: Schema.

Where to go next

On this page