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