Skip to main content
The Project module renders a customer’s onboarding project — its milestones, tasks, progress, and editable fields — and lets the customer update them. Use the pre-assembled component, drop to the headless hook, or compose your own layout from primitives. Everything here reads connection + auth from <StatisfyProvider> (except the standalone createProjectClient).
The project endpoints require a portal-session token — one carrying a customer_id claim, which only the portal sign-in flow (POST /auth/verify / POST /auth/token) produces. If your backend mints the bearer itself (the vendored path), these calls return 401 not_a_portal_session; the Digital Worker chat still works. See Authentication.

Two components, two resolution stories

ProjectModule and OnboardingProject render the same assembled project view (header, progress meter, the plan grouped by milestone, comments, task-detail drawer) — they differ only in which project they show:
  • <OnboardingProject configId="onboarding" /> — the signed-in account’s own project, read from the Onboarding Project field on their Account. This is the common case: each customer sees their own progress.
  • <ProjectModule configId="…" /> (or projectId outside a portal) — one project the portal’s author pins, in Portal Studio’s Settings. The pinned project resolves against the signed-in account, so only users of the account that owns it see it — every other account gets the empty state. This is intended, not a bug: use it only when every viewer should land on the same project.
projectId as a prop is embed-mode only — for rendering outside a portal, where there’s no portal.runtime.json to read from. Inside a portal template, the portal validator rejects a projectId prop next to a configId on ProjectModule (IDENTITY_PROP_FORBIDDEN), because a configId makes the config file the only value channel a Settings edit can trust. OnboardingProject has no projectId prop to conflict with — its only prop besides className is the required configId — so this doesn’t apply to it.

OnboardingProject

It distinguishes three states: resolving (renders nothing), resolved with the field unset (“No onboarding project is set up for your account yet.”), and a failed config fetch (a distinct message) — so a transient network error is never reported to a customer as “you have no project.”

ProjectModule

It renders a header, a progress meter, and the plan — milestone groups of task rows leading with each group’s ordinal, its open-step count and its due date, with each row showing a lifecycle marker, the task title, the due date, and its status pill. Editing happens in the task-detail drawer.
The stepper (<MilestoneJourney>) is no longer composed into ProjectModule / OnboardingProject — the milestone groups already carry the ordinal, state and dates the stepper would repeat. It’s still exported for a surface that wants it; render it yourself when you need it. Since SDK 0.1.1, hosts embedding either component see the plan where the stepper used to sit — a portal picks this up on its next build.
A task row is a single clickable button that opens the task-detail drawer; there is no tick-to-complete control on the row itself. Completing a task means writing a status, and the row can’t know which of a tenant’s status options counts as “done” — so the marker on the row is an indicator, and the drawer’s <StatusSelect> is the control.

Headless: useProject

For full control of the layout, use the hook. It resolves the project (by id, or via config when you omit the id), exposes the data, and provides optimistic mutations.

UseProjectResult

updateTask / updateProject apply your change optimistically (the UI updates immediately), then reconcile with the server’s full response — and roll the row back if the request fails. They reject on failure so you can show an inline error.
Send only the fields you’re changing (a diff), and only editable fields. A non-editable or unknown key is rejected with 400 not_editable; a bad value with 400 invalid_option / 400 invalid_type.

Composable primitives

Build a bespoke layout from the same building blocks OnboardingProject and ProjectModule use. Each is schema-driven and styled with the SDK’s theme. Two non-component helpers round it out:
  • formatFieldValue(schema, properties, display) — the display string for one field, usable outside React.
  • useComments(projectId, taskId?) — the headless hook behind Comments, returning { comments, loading, error, posting, addComment, reload }.
These props are positional in meaning, not interchangeable: TaskList takes milestones + tasksByParent (not a flat tasks array) and onSelectTask (not an update callback) — it selects a task, it doesn’t write. Writes go through useProject’s updateTask.

Comments

Drop a comment thread onto a project or a single task. TaskDetail already includes one; use Comments directly when you’re composing your own layout.
Comments come back in server order (oldest first). addComment appends the server row on success and rejects on failure, so you can surface an inline error.

Standalone client: createProjectClient

When you want project data outside React (or your own state layer), use the client directly. It doesn’t need the provider — pass config explicitly.
Comments posted through the SDK are always customer-visible (external). Editing a comment is REST-only — PATCH /sdk/v1/projects/{id}/comments/{commentId}, author-only — and does not yet have a client method.

Data shapes

See the API Reference for endpoints, headers, and error codes.