Skip to content

Components

All from @clipless/react. Conditions (renderIf, showIf, requiredIf, disabledIf, condition) use the syntax in Concepts.

<Workflow>

The root. Holds the session and renders the stage the viewer owns.

Prop
idthe workflow id you deploy under, e.g. expense_claim_v1
versionyour label for this version (informational)
runtimeTokena session token from your backend — connects to the engine
serverUrlthe engine's base URL (with runtimeToken)
actorlocal mode: who is viewing, { role }
initialAnswerslocal mode: answers to start with (written like the engine's prefill)
carriedlocal mode: { from, answers } carried from an earlier run — see Recurring
initialEventslocal mode: resume from a saved ledger
transportlocal mode: createLocalTransport({ actions }) to stub actions
onCompletecalled with the answers when the session completes
onStoreReadyreceives the underlying store (devtools, tests)
componentRegistryreplace default field components by type: { date: MyDatePicker }

Without runtimeToken, the whole engine runs in the browser and nothing is saved.

Stubbing actions locally

tsx
const transport = createLocalTransport({
  actions: {
    risk: (answers) => ({ result: { level: answers.amount > 10_000 ? 'high' : 'low' } }),
    kyc: () => ({ pending: true, result: { url: '#' }, settle: wait(2000).then(() => ({ result: { status: 'verified' } })) }),
  },
})

A handler returns { result }, { error }, or { pending: true, result?, settle } to behave like an action that finishes later. Unstubbed actions succeed with no result.

<Stage>

Prop
namestage id
actorthe owning role, e.g. role:manager — a string literal
labeldisplay name
renderIfcondition for the stage to be part of the path; otherwise it is skipped
transitions[{ targetStageId, condition? }] — where to go on completion; see Loop-backs
freshstart every round empty (a review decides again)
visibleToroles that may read its answers besides the owner; default everyone
mode'live' (default), 'readonly' or 'replay' — the latter two refuse answers

Renders its children only when it is the active stage and the viewer owns it.

<Step>

Prop
namestep id
labeldisplay name (for your stepper)
showIfcondition to include the step

Fields directly inside a <Stage> without a <Step> go into one implicit step.

<Field>

Prop
namefield id
typeone of the types below (string is text)
label, placeholder
required, requiredIfmust be answered (always / when a condition holds)
mustBeTruea checkbox that passes only when ticked; implies required
showIf, disabledIfhidden fields are skipped; disabled ones are not validated
min, max, minLength, maxLength, pattern, stepconstraints
options[{ value, label? }] for select, radio, multicheck, ranking, matrix columns
multiple, minSelect, maxSelect, rowsselection and matrix settings
format, mask, maskedinput masks (ssn, phone, zip, ein, …) and hide-on-blur
accept, maxSizefile fields: allowed types (.pdf,image/*) and bytes — see File uploads
vaultencrypt at rest with the session's key
piipersonal data: kept out of caches, masked in developer tools
visibleToroles that may read it; 'everyone' re-opens a field of a restricted stage
carryOver={false}never carried into a new run of a recurring workflow

Types: text textarea email tel url number currency date datetime time yearmonth select radio checkbox multicheck boolean yesno rating tags address georanking matrix signature file hidden custom.

Your own input

With no children, a field renders the default component for its type. Otherwise, children can be:

tsx
{/* an element: value, onChange, onBlur, name are wired in */}
<Field name="email" type="email"><input className="my-input" /></Field>

{/* a component that reads the field itself */}
<Field name="email" type="email"><MyEmailInput /></Field>
function MyEmailInput() {
  const f = useField() // value, onChange, onBlur, error, touched, required, disabled, isReadOnly
  return <TextField {...f.bindings} error={f.error} />
}

{/* a render function */}
<Field name="email" type="email">{(f) => <TextField {...f.bindings} />}</Field>

<FieldGroup>

A repeatable group — its value is a list of items.

tsx
<FieldGroup name="dependents" minItems={0} maxItems={5}>
  {({ id }) => (
    <>
      <Field name="name" type="text" required />
      <Field name="age" type="number" />
    </>
  )}
</FieldGroup>

useFieldGroup('dependents') gives { items, add, remove, canAdd, canRemove } for your buttons. Each item has its own fields, and the engine validates every item against that item's answers. A showIf inside an item reads its siblings, such as a student question asked only of a dependent whose relationship is Child. minItems and maxItems are checked on submit. The tax return in the playground has dependents and businesses, with collapsed previews.

<Action>

Declared inside the <Stage> or <Field> that triggers it. See Actions for every type.

Prop
idthe name its result is stored under (useAction(id), conditions)
typewebhook email otp_send otp_verify ai_prompt kyc pdf
onin a stage: "enter", or by default when it is submitted. In a field: "valid" (default), "change", "blur", "enter_key" — see Field actions
blockinga failure stops the stage from completing
visibleToroles that may read its result
timeoutSecondshow long it may wait for an outcome (default 24 h)
the type's own settings

<Remarks>

Notes on a field, kept beside the answer rather than in it. A remark can explain an answer, or be a note one reviewer leaves for another. The engine accepts a remark only where the active stage declares a <Remarks> for that field.

Try it

An expense claim with remarks on the amount in the playground runs in your browser. Play every role from the toolbar.

tsx
// The employee explains their answer. Anyone who can read `amount` reads the remark.
<Field name="amount" type="number">
  <Remarks />
</Field>

// The manager's notes on the employee's amount, for finance only
<Stage name="review" actor="role:manager">
  <Remarks on="amount" visibleTo={['role:finance']} />
</Stage>
Prop
onthe field (optional inside a <Field>). It can be any stage's top-level field that the stage's owner can read
visibleTowho reads these remarks besides their author. Default: whoever can read the field
labelheading for the default UI
children(remarks) => ReactNode renders your own UI (see useRemarks)

The default UI is a comment icon on the field, showing how many remarks there are. Clicking it opens a popup with the thread this viewer can read and, while they're allowed to add, a box to write a new remark. Inside a <Field> the icon sits at the end of the label row. With on, it appears wherever you place the tag, such as beside the value in a summary of an earlier stage's answers. Each remark is a remark event in the ledger, stamped with the stage it was left from. That stage's visibleTo decides who receives it. Deploy refuses a <Remarks> on a field that doesn't exist or that the stage's owner can't read.

Remarks are not encrypted

A remark is stored as plain text, even on a vault or pii field. Don't ask for personal data in one. Declare <Remarks> where people explain or discuss an answer, not where they would repeat it.