Human-in-the-Loop
Pause workflows for user input, review, or confirmation using wait: true transitions and document UI actions.
Wait Transition Pattern
A transition with wait: true pauses the workflow until externally triggered by user interaction:
@Transition({
from: 'waiting_for_user',
to: 'ready',
wait: true,
schema: z.object({ message: z.string() }),
})
async userMessage(payload: { message: string }) {
await this.repository.save(ClaudeMessageDocument, {
role: 'user',
content: payload.message,
});
}Document Action Buttons
Documents can include buttons that trigger wait: true transitions:
# Document YAML
type: document
ui:
widgets:
- widget: form
options:
properties:
text:
title: Text
widget: textarea
actions:
- type: button
transition: userResponse # Must match the method name
label: 'Submit'When the user clicks Submit, the workflow’s userResponse method fires with the document’s current content as the payload.
Chat Input Widget
For conversational UIs, use the prompt-input widget:
ui:
widgets:
- widget: prompt-input
enabledWhen:
- waiting_for_user
options:
transition: userMessage@Transition({
from: 'waiting_for_user',
to: 'ready',
wait: true,
schema: z.string(),
})
async userMessage(payload: string) {
await this.repository.save(ClaudeMessageDocument, {
role: 'user',
content: payload,
});
}Confirmation Pattern
Show AI-generated content for user review before proceeding:
@Workflow({
uiConfig: __dirname + '/meeting-notes.ui.yaml',
schema: z.object({ inputText: z.string().default('...') }),
})
export class MeetingNotesWorkflow extends BaseWorkflow {
@InjectTool() claudeGenerateDocument: ClaudeGenerateDocument;
@Initial({ to: 'waiting_for_response' })
async createForm() {
const args = this.ctx.args as { inputText: string };
await this.repository.save(
MeetingNotesDocument,
{
text: args.inputText,
},
{ id: 'input' },
);
}
// Wait for user to edit and submit
@Transition({ from: 'waiting_for_response', to: 'response_received', wait: true })
async userResponse() {
const payload = this.ctx.runtime.transition!.payload;
await this.repository.save(MeetingNotesDocument, payload, { id: 'input' });
}
// AI generates structured output
@Transition({ from: 'response_received', to: 'notes_optimized' })
async optimizeNotes() {
await this.claudeGenerateDocument.call({
claude: { model: 'claude-sonnet-4-6' },
response: { id: 'final', document: OptimizedNotesDocument },
prompt: `Structure these notes...`,
});
}
// Wait for user to confirm
@Final({ from: 'notes_optimized', wait: true })
async confirm() {
const payload = this.ctx.runtime.transition!.payload;
await this.repository.save(OptimizedNotesDocument, payload, { id: 'final' });
}
}enabledWhen — Conditional Widgets
Show/hide widgets based on the current workflow place:
ui:
widgets:
- widget: form
enabledWhen:
- review
- editing
options:
properties:
summary:
title: Summary
widget: textarea
actions:
- type: button
transition: confirm
label: 'Confirm'The widget only appears when the workflow is at the review or editing place.
Registry References
- meeting-notes-example-workflow — Full human-in-the-loop workflow with editable form, AI optimization, and user confirmation
- chat-example-workflow — Chat input pattern with prompt-input widget
Last updated on