Skip to content

Workflow

Autosave, drafts, wizards, progress, and related workflow config.

Previous: State · Next: Rules

Playground

Workflow explorer → — autosave debounce, draft restore, wizard steps.

Problem → solution

ProblemSolution
Debounced draft save duplicated per formworkflow.autosave
Lose progress on reloadworkflow.draft
Multi-step forms with per-step validationworkflow.wizard

Overview

FeatureUse case
autosaveDebounced persistence on value change
draftRestore values after navigation or reload
wizardStep index with per-step validation
offlineQueueQueue submits while offline (Submission)
keyboardShortcut actions (Integrations)
analyticsDrop-off / error snapshots (Integrations)

Conditional show/hide lives in Rules (when()), not in workflow config.


Autosave

ts
createForm({
  initialValues: { notes: "" },
  workflow: {
    autosave: {
      enabled: true,
      debounceMs: 500,
      onSave: async (values) => {
        await api.saveDraft(values);
      },
    },
  },
});

Status: form.state.workflow.isAutosaving, form.state.workflow.lastAutosaveAt.

Plugin hook: api.on("onAutosave", …) — see Plugins.


Draft restore

Persists to storage and merges on next visit:

ts
workflow: {
  draft: {
    enabled: true,
    storageKey: "signup-draft-v1",
    onRestore: (values) => console.log("Welcome back!", values),
    // promptOnRestore: true,
    // onRestorePrompt: (values) => window.confirm("Restore draft?"),
  },
},

Manual: form.saveDraft().

Try it

Fill fields in the Workflow playground, reload — the draft comes back.


Multi-step wizard

Each step needs a string id and the fields to validate on next():

ts
workflow: {
  wizard: {
    initialStep: 0,
    steps: [
      { id: "profile", fields: ["name", "email"] },
      { id: "details", fields: ["bio"] },
    ],
  },
},

Navigate:

ts
await form.workflow.next(); // validates current step first
form.workflow.prev();
await form.workflow.goTo(1);

Progress UI:

ts
const w = form.state.workflow;
w.currentStep; // 0-based
w.totalSteps;
w.canGoNext;
w.canGoPrev;
w.progress; // 0–100

Element structure (wizard)

Render one step panel at a time from workflow.currentStep.

React JSX

tsx
const step = form.state.workflow.currentStep;

return (
  <form {...form.form()}>
    <div className="progress" style={{ width: `${form.state.workflow.progress}%` }} />

    {step === 0 ? (
      <section>
        <input {...form.field("name")} placeholder="Name" />
        <input {...form.field("email")} placeholder="Email" />
      </section>
    ) : (
      <section>
        <textarea {...form.field("bio")} placeholder="Bio" />
      </section>
    )}

    <button
      type="button"
      disabled={!form.state.workflow.canGoPrev}
      onClick={() => form.workflow.prev()}
    >
      Back
    </button>
    <button
      type="button"
      disabled={!form.state.workflow.canGoNext}
      onClick={() => void form.workflow.next()}
    >
      Next
    </button>
    {step === form.state.workflow.totalSteps - 1 ? (
      <button {...form.submit()}>Finish</button>
    ) : null}
  </form>
);

Native HTML

html
<form id="wizard">
  <div data-step="0">
    <input name="name" />
    <input name="email" />
  </div>
  <div data-step="1" hidden>
    <textarea name="bio"></textarea>
  </div>
  <button type="button" data-action="prev">Back</button>
  <button type="button" data-action="next">Next</button>
  <button type="submit">Finish</button>
</form>

Toggle [data-step] visibility from form.state.workflow.currentStep in your subscribe() render loop (or framework adapter).


Cheat sheet

ts
form.saveDraft();
form.state.workflow.isAutosaving;
await form.workflow.next();
form.workflow.prev();
form.state.workflow.progress;

Next: Ruleswhen() show/hide/require/populate.

JOC is a JayOnCode monorepo of @jayoncode/* packages. Docs sync from source via TypeDoc and sync scripts.