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 CLI Web or API.
While a Conversation is waiting for input, shared runtime UI hides the bottom message composer and keeps the waiting form available until the user submits or cancels it.
The form field reuses the ParamDefinition 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 RunPayload>(
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 | ParamDefinition[] | 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 | NodeMetadataInput | The node displays meta information. |
Default payload
interface UserInputPayload extends RunPayload {
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 RunPayload, 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 | The local runtime UI submits { 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; steamharness json --run-id ... --resolved-user-inputs-json ... recovery is required. |
waiting_for_input is a persistent, non-final state. Resume with the locally saved pendingUserInput, the same runId, and the original entrypointId; do not switch to a new default entrypoint. Conversation retains a single entry and rejects entrypoint parameters. Server does not save or resume runs.
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 RunPayload {
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 deployProject = workflow.defineProject<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);
},
});