createUserInputNode
workflow.createUserInputNode() Creates a human input node. When the node executes, it will first check whether the current run has a submission answer corresponding to requestId; If not, it will publish the user_input_requested event to runtime, save run as waiting_for_input, and display the waiting form in Desktop, server embed or API.
The form field reuses the WorkflowParamDefinition of the current Workflow entrypoint(Conversation is executor), and does not create another schema for the user input node.
Wait forms also support date, time, datetime, and date-range. The submitted values remain YYYY-MM-DD, HH:mm:ss, local YYYY-MM-DDTHH:mm:ss, and closed interval YYYY-MM-DD/YYYY-MM-DD strings, respectively, so workflow can read directly from payload.values[param.name] without receiving a Date object or a time zone-converted value. The date field must use type: "string", can only be placed in main or auxiliary, cannot declare enumeration options or multiple: true; Required blank values or formatting errors will remain in the form to display errors and prevent submission until the user corrects them.
Signature
workflow.createUserInputNode<Input, Output extends WorkflowPayload>(
options: UserInputNodeOptions<Input, Output>,
): BaseNode<Input, Output>
options
| Field | Type | Description |
|---|---|---|
name | string | Node name, default "user-input". will be used for report, requestId, and Diagram. |
title | string | Wait for the form title. Use name when not passed. |
description | string | Wait for the form description. |
params | WorkflowParamDefinition[] | Form field, field definition is consistent with params of the selected Workflow entry;Conversation uses executor.params. |
defaultValues | Record<string, unknown> | Form default, match by param name. |
resolveRequest | (input, context) => UserInputRequestDefinition | Promise<UserInputRequestDefinition> | Dynamically generate title, description, params, and defaultValues based on the current node input. |
createRequestId | (input, context) => string | The custom request ID. The default contains workflow, runId, and node name. |
format | (payload, input, context) => Output | Convert the default submission payload to the business payload. |
historyLimit | number | The number of nodes retained in the execution history. The default value is 50. |
metadata | WorkflowNodeMetadataInput | The node displays meta information. |
Default payload
interface UserInputPayload extends WorkflowPayload {
requestId: string;
values: Record<string, unknown>;
submittedAt: string;
}
Default return on successful recovery:
{
errCode: 0,
errMessage: "",
requestId,
values,
submittedAt
}
If format is passed in, the return value must still inherit WorkflowPayload, using errCode: 0, errMessage: "" on success.
Runtime behavior
| Phase | Behavior |
|---|---|
| First execution | When no value is submitted, user_input_requested is published, pendingUserInput is reported, and the run status changes to waiting_for_input. |
| Submit answer | Desktop, server, or embed commit { requestId, values }. |
| Resume Execution | runtime injects resolvedUserInputs with the same runId, the node publishes user_input_resolved and returns the payload. |
| Non-interactive CLI | CLI does not read stdin; workflow-code json --run-id ... --resolved-user-inputs-json ... recovery is required. |
waiting_for_input is a non-final state, but also a persistent state. After the server restarts, you can still read pendingUserInput from run record and accept the commit; stale running logic does not misjudge it as a timeout.
The Workflow recovery must continue to use the exact resolvedTarget and the original entrypointId in the run record. The recovery request cannot switch entries; if draft deletes the original entry during the wait period,Server returns 409 and does not use the new default entry instead. Only when the old run record has no entry field, the default entry of the precise target is compatible with recovery. Conversation keeps the single entry and also does not accept entry parameters.
When the problem field is dynamically generated by an external agent, remote approval system, or current node input, you can keep static params as the default definition for structural analysis and use resolveRequest to generate this waiting form. resolver can be executed asynchronously, but must be deterministic and have no side effects on the same input; the field names and option values returned must be stable when the same requestId is restored.
const questionNode = workflow.createUserInputNode<AgentQuestion>({
name: "agent-question",
params: [],
resolveRequest(question) {
return {
title: question.header,
description: question.prompt,
params: [{
name: "answer",
type: "string",
control: question.multiple ? "checkboxes" : "radio",
multiple: question.multiple,
required: true,
options: question.options,
}],
};
},
createRequestId(question) {
return `agent-question:${question.id}`;
},
});
Example
interface ApprovalPayload extends WorkflowPayload {
approved: boolean;
note: string;
reviewDate: string;
}
const approvalNode = workflow.createUserInputNode<Plan, ApprovalPayload>({
name: "approval",
title: "Review deployment",
description: "Confirm whether the workflow should continue.",
params: [
{ name: "approved", flag: "--approved", type: "boolean" },
{ name: "note", flag: "--note", type: "string", control: "textarea" },
{ name: "reviewDate", flag: "--review-date", type: "string", control: "date", required: true },
],
defaultValues: { approved: false, reviewDate: "2026-07-26" },
format(payload) {
return {
errCode: 0,
errMessage: "",
approved: payload.values.approved === true,
note: typeof payload.values.note === "string" ? payload.values.note : "",
reviewDate: typeof payload.values.reviewDate === "string" ? payload.values.reviewDate : "",
};
},
});
export const deployWorkflow = workflow.defineWorkflow<Input, OutputPayload>({
name: "deploy",
async run(input, context) {
const plan = await workflow.runNode(planNode, input, context);
const approval = await workflow.runNode(approvalNode, plan, context);
if (!approval.approved) {
return workflow.createOutputPayload({
items: [{ title: "Stopped", content: approval.note, contentType: "text" }],
});
}
return workflow.runNode(deployNode, plan, context);
},
});