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 | |
|---|---|
id | the workflow id you deploy under, e.g. expense_claim_v1 |
version | your label for this version (informational) |
runtimeToken | a session token from your backend — connects to the engine |
serverUrl | the engine's base URL (with runtimeToken) |
actor | local mode: who is viewing, { role } |
initialAnswers | local mode: answers to start with (written like the engine's prefill) |
carried | local mode: { from, answers } carried from an earlier run — see Recurring |
initialEvents | local mode: resume from a saved ledger |
transport | local mode: createLocalTransport({ actions }) to stub actions |
onComplete | called with the answers when the session completes |
onStoreReady | receives the underlying store (devtools, tests) |
componentRegistry | replace 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 | |
|---|---|
name | stage id |
actor | the owning role, e.g. role:manager — a string literal |
label | display name |
renderIf | condition for the stage to be part of the path; otherwise it is skipped |
transitions | [{ targetStageId, condition? }] — where to go on completion; see Loop-backs |
fresh | start every round empty (a review decides again) |
visibleTo | roles 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 | |
|---|---|
name | step id |
label | display name (for your stepper) |
showIf | condition to include the step |
Fields directly inside a <Stage> without a <Step> go into one implicit step.
<Field>
| Prop | |
|---|---|
name | field id |
type | one of the types below (string is text) |
label, placeholder | |
required, requiredIf | must be answered (always / when a condition holds) |
mustBeTrue | a checkbox that passes only when ticked; implies required |
showIf, disabledIf | hidden fields are skipped; disabled ones are not validated |
min, max, minLength, maxLength, pattern, step | constraints |
options | [{ value, label? }] for select, radio, multicheck, ranking, matrix columns |
multiple, minSelect, maxSelect, rows | selection and matrix settings |
format, mask, masked | input masks (ssn, phone, zip, ein, …) and hide-on-blur |
accept, maxSize | file fields: allowed types (.pdf,image/*) and bytes — see File uploads |
vault | encrypt at rest with the session's key |
pii | personal data: kept out of caches, masked in developer tools |
visibleTo | roles 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 | |
|---|---|
id | the name its result is stored under (useAction(id), conditions) |
type | webhook email otp_send otp_verify ai_prompt kyc pdf |
on | in a stage: "enter", or by default when it is submitted. In a field: "valid" (default), "change", "blur", "enter_key" — see Field actions |
blocking | a failure stops the stage from completing |
visibleTo | roles that may read its result |
timeoutSeconds | how 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 | |
|---|---|
on | the field (optional inside a <Field>). It can be any stage's top-level field that the stage's owner can read |
visibleTo | who reads these remarks besides their author. Default: whoever can read the field |
label | heading 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.