Skip to content

Human-Gate Steps

Sovereign
Sovereign tier required. Human gates keep final authority with the operator while the surrounding process remains automated.

Use a human_gate when the workflow needs an authorized operator to approve or reject a decision, or answer questions before work continues. The run inspector presents the authored prompt and enables input only for an authorized operator.

Configuration

- key: approve_release
  name: Approve Release
  kind: human_gate
  needs: [prepare_release]
  humanGate:
    prompt: "Approve ${steps.prepare_release.outputs.releaseName}?"
    approvalText: Ship
    rejectionText: Hold
  waitsFor:
    kind: workflow_input
    expected: [approved, rejected]
    authorizedHandles: ["@release-operator"]
  outputs:
    outcome: "$.normalized.input.outcome"
FieldRequiredPurpose
humanGate.enabledNoSet false only when this version intentionally bypasses the gate
humanGate.promptNoDecision shown to the operator
humanGate.approvalTextNoApproval button label
humanGate.rejectionTextNoRejection button label
humanGate.questionsQuestions modeLiteral question array or one exact reference to an array
humanGate.referencesNoUp to eight labeled task, session, run, or URL links
humanGate.previewNoOne to five supporting documents for an approval or Questions gate
humanGate.feedbackNoWriter session, task, and review that receive requested corrections; requires at least one preview document
humanGate.pullRequestProposalNoEditable pull-request proposal backed by frozen branch evidence
waitsFor.expectedYesApproval gates accept one or both of approved, rejected; Questions gates require answered and may include rejected
waitsFor.authorizedHandlesYesOperators allowed to decide
outputsNoNamed outcomes or input values for later steps

When copy is omitted, approval gates use the step name, Approve, and Reject. Questions gates use the step name, Submit answers, and Reject.

Ask questions

Select a Human Gate step in the Builder, open Approval content in its inspector, and choose Questions to collect structured answers while the run waits. Questions require an enabled gate. Enter a literal list or an exact reference to an array produced by an earlier step; either way, the resolved value must follow the same question format. For generated questions, the reference must resolve to an array, not JSON text:

humanGate:
  questions: "${steps.ask.outputs.questions}"

Add 1–12 questions with unique IDs; each can be text, single_choice, multiple_choice, or yes_no. Use descriptive question and option IDs, such as change_type and bug_fix. IDs start with a letter and may contain letters, numbers, _, or - (up to 64 characters). If the Builder flags an ID, choose a different descriptive name. Question labels must be nonblank and at most 1,000 characters. Choice questions need 2–20 options with unique IDs and nonblank labels of up to 300 characters. Questions are required by default; set required: false to let the operator leave one unanswered. Questions can include document previews for context. Use separate gates for feedback, pull-request proposals, and session-output evidence.

- key: clarify_change
  name: Clarify Change
  kind: human_gate
  humanGate:
    prompt: "Give the next agent the context needed to continue."
    questions:
      - id: change_type
        type: single_choice
        label: "What kind of change is this?"
        options:
          - { id: bug_fix, label: Bug fix }
          - { id: feature, label: Feature }
      - id: extra_context
        type: text
        label: "What should the next agent know?"
        required: false
  waitsFor:
    kind: workflow_input
    expected: [answered, rejected]
    authorizedHandles: ["@release-operator"]
  onFailure:
    action: block
  outputs:
    outcome: "$.normalized.input.outcome"
    questions: "$.normalized.input.questions"
    answers: "$.normalized.input.payload.answers"

Add document context

When an agent’s specification, brief, or decision file helps explain the questions, add it through the Builder’s Preview documents controls in the same Approval content inspector. A Questions gate can show one to five documents alongside its questions:

humanGate:
  questions: "${steps.write_spec.outputs.questions}"
  preview:
    documents:
      - key: spec
        label: Specification
        path: "${steps.write_spec.outputs.specPath}"
        sourceSessionUlid: "${steps.write_spec.outputs.sessionUlid}"

Add write_spec to the gate’s needs and declare its questions, specPath, and sessionUlid outputs. The document path is read from the session that produced it; use that session’s output for sourceSessionUlid.

The operator selects a Context document above the questions and can choose Reload context to read its current contents. Switching documents, reloading, or encountering a preview error keeps drafted answers. If a preview cannot load, its error is shown with a reload control; the operator can still answer the questions. Submitting answers records clarification, not approval of the document.

Use the answers

The operator can answer each question in its declared format or choose Write my own answer for a written response. A written answer is valid for any question, including a choice or yes/no question. Required questions need an answer; text answers must be nonblank and are limited to 10,000 characters. Optional questions may be omitted. After an answered submission, the questions shown to the operator and their answers are available as step outputs, with answers keyed by question ID, as shown above. A text response can answer a choice question; for example, the change_type response can be:

{
  "answers": {
    "change_type": { "type": "text", "value": "A regression affects the export path." }
  }
}

Map the question list and answers into a later agent’s prompt so it can use both the original questions and responses:

- key: continue_change
  name: Continue Change
  kind: session
  needs: [clarify_change]
  session:
    action: launch_prompt
    prompt: |
      Continue the change using the operator's answers.
      Questions: ${steps.clarify_change.outputs.questions}
      Answers: ${steps.clarify_change.outputs.answers}
      Finish with a JSON object whose outcome is change_completed.
    launcherConfigKey: coder-codex
    role: executor
  waitsFor:
    kind: workflow_input
    expected: [change_completed]
  gates:
    - key: completion
      kind: session_output
      source:
        event: final_agent_message
      extract: { format: json, path: $.outcome }
      outcomes: { change_completed: change_completed }
      onInvalid: { action: block }
  outputs:
    sessionUlid: "$.normalized.session.sessionUlid"
    outcome: "$.normalized.input.outcome"

The gate requires an answered outcome and may also offer rejected; it does not accept approved. Rejection does not stop the run by itself: route it to the intended follow-up branch, or set onFailure.action: block when the run should stop at the gate. Submitting answers records input; it does not approve a diff or publish work.

The question list is set when the gate opens and stays the same while it waits for answers. If authored or generated question data is missing or invalid, the run blocks with a visible recovery message. A running workflow keeps its selected version; correct invalid questions in a new workflow version and start a new run. Draft answers stay in the current browser, survive refreshes, closing the dialog, and submission failures, and do not sync to another device. If Valdr reports that input was recorded but the workflow could not advance, check the refreshed run status before acting again; do not submit the same answers again. If the run is still waiting after an uncertain result, refresh its status and submit the retained draft again.

humanGate.enabled: false intentionally bypasses the decision and records the step as approved without waiting for human input. The step must still include approved in waitsFor.expected. Use a new workflow version when changing this setting. A run already waiting at an enabled gate still requires its operator decision.

Link supporting records

References give the operator direct context without changing the decision contract:

humanGate:
  references:
    - kind: task
      target: "${workflow.inputs.taskKey}"
      label: Open task
    - kind: run
      target: "${runtime.runUlid}"
      label: Open run

Each reference needs kind, target, and label. Supported kinds are task, session, run, and url.

Preview reviewed documents

A gate can present one to five documents before the operator decides:

humanGate:
  prompt: "Approve the reviewed specification?"
  approvalText: Approve spec
  rejectionText: Reject
  preview:
    documents:
      - key: spec
        label: Specification
        path: "${steps.create_spec.outputs.path}"
        sourceSessionUlid: "${steps.create_spec.outputs.writerSessionUlid}"

Each document needs a unique key, a visible label, its path, and the session that created it.

Send feedback before deciding

humanGate:
  feedback:
    sourceSessionUlid: "${steps.create_spec.outputs.writerSessionUlid}"
    taskKey: "${workflow.inputs.taskKey}"
    reviewId: "${steps.create_spec.outputs.reviewId}"

Feedback returns a correction request to the configured writer session and refreshes the preview. It does not approve or reject the gate. The operator reviews the updated document, then makes the final decision.

The Builder exposes structured Preview documents and Feedback controls. Use its YAML dialog when you need to inspect or edit the full definition directly.

Approve a pull-request proposal

A pull-request gate lets the operator edit the title, body, and draft state before approving publication. Bind it to frozen repository evidence:

humanGate:
  prompt: Review and approve the pull request proposal.
  approvalText: Publish pull request
  rejectionText: Reject
  pullRequestProposal:
    repository: "${steps.observe_remote.outputs.repositoryOwner}/${steps.observe_remote.outputs.repositoryName}"
    remoteName: "${workflow.inputs.remoteName}"
    baseBranch: "${workflow.inputs.baseBranch}"
    headBranch: "${workflow.inputs.headBranch}"
    headSha: "${steps.resolve_head.outputs.resolvedSha}"
    writerSessionUlid: "${steps.draft_proposal.outputs.sessionUlid}"
    evidenceDigest: "${steps.read_diff.outputs.digest}"
    evidence:
      runUlid: "${steps.read_diff.outputs.runUlid}"
      stepRunUlid: "${steps.read_diff.outputs.stepRunUlid}"
    initial:
      title: "${steps.draft_proposal.outputs.title}"
      body: "${steps.draft_proposal.outputs.body}"
      draftFrom: "${workflow.inputs.initialDraft}"

evidence.runUlid and evidence.stepRunUlid are required and identify the run and step that captured the frozen evidence. Bind them to the same evidence-producing step as evidenceDigest.

draftFrom must be an exact reference; the referenced value must resolve to a Boolean at runtime. You may use a literal Boolean in initial.draft instead. Initial and edited titles must be one line and at most 256 characters; bodies must use LF line endings and be at most 65,536 bytes. Map $.normalized.input.payload.title, .body, and .draft as step outputs when the later GitHub step should publish the operator-approved values.

Route the decision

- key: release
  name: Release
  kind: condition
  needs: [approve_release]
  when:
    - kind: value_equals
      value: "${steps.approve_release.outputs.outcome}"
      equals: approved
  outputs:
    outcome: "${steps.approve_release.outputs.outcome}"

This example publishes the decision only on the approved branch; replace the condition with your intended follow-up action when composing the workflow. A rejection is recorded as the rejected workflow value. Without a block or fail policy, route it to the appropriate follow-up step. Set onFailure.action to block or fail on the human gate when rejection should stop the run there.

Next step

Continue with Outcome route to route the decision explicitly.