Skip to content

Human requests

Park a Loop at an ask node, inspect its redacted context, and admit one validated answer from a human or permitted agent.

For people running agent work18 pages in this section

An ask control pauses one node cell without occupying an agent session. Its rendered prompt, redacted context, answer shape, responder policy, and expiry are frozen when the request opens. The admitted payload becomes that node's output, so downstream nodes read it through the usual nodes.<id>.output namespace.

Author an ask

graph:
  nodes:
    - id: choose_environment
      class: control
      kind: ask
      params:
        prompt: "Where should {{ .inputs.release }} deploy?"
        context:
          release: "{{ .inputs.release }}"
          candidates: [staging, production]
        expect:
          type: object
          required: [environment, assignment]
          properties:
            environment:
              type: string
              enum: [staging, production]
            assignment:
              type: object
              required: [reviewer]
              properties:
                reviewer:
                  type: string
                  x-compozy-kind: agent
        responders:
          agents: deny
        expires:
          after: 72h
          route: choose_environment_result

    - id: choose_environment_result
      class: control
      kind: route
      routes:
        - when: nodes.choose_environment.output != null
          to: deploy
      default: request_expired

    - id: deploy
      class: action
      kind: transform
      params:
        map:
          environment:
            template: "{{ .nodes.choose_environment.output.environment }}"

    - id: request_expired
      class: action
      kind: transform
      params:
        map:
          expired: { value: true }

  edges:
    - { from: choose_environment, to: choose_environment_result }
    - { from: choose_environment_result, to: deploy }
    - { from: choose_environment_result, to: request_expired }

expect is required and becomes the output shape. A string property may add x-compozy-kind with agent, skill, loop, worktree, session, workspace, or secret. The annotation is checked recursively in nested objects and arrays. It drives the same catalog control used by Loop inputs and requires the submitted string to name an entity that exists in the request's workspace; secret uses redacted global Vault-reference metadata. An enum on the same property wins over the catalog.

responders.agents defaults to deny; set it to allow only when capability-gated agents should answer. An agent still cannot answer a run it started, including through a spawned-child chain. expires.route must be a direct forward edge.

Set loops.defaults.delivery.requests.expire_after or loops.defaults.watch.requests.expire_after to seed asks that omit expires. An authored expiry always wins. Leaving the setting empty keeps expiry entirely in the definition.

Operate requests

ActionCLIHTTP or UDSNative tool
Listcompozy loop requests --state pendingGET /loop-requests?state=pendingcompozy__loop_requests
Detailcompozy loop request --run-id <id> --generation <n> --node <id>GET /loop-runs/:run_id/nodes/:node_id/request?generation=<n>compozy__loop_request
Answercompozy loop respond --run-id <id> --generation <n> --node <id> --payload '{"environment":"production"}'POST /loop-runs/:run_id/nodes/:node_id/respondcompozy__loop_respond

The list returns bounded previews and aggregates.pending; detail returns the full redacted context. Detail and answer calls require the request generation. Fan-out requests also use --item or item_index. Pending order is expiry first, with requests without expiry last; resolved order is newest first. Cursors preserve that order.

Pending requests also appear under Needs you in the OS attention bell and carry a matching indicator in the Loop runs list. The bell badge uses the daemon's pending aggregate rather than the number of loaded rows. Selecting a request switches to its workspace, opens the run, and focuses the request form; the run indicator counts every pending page for that run. Answering, canceling, or expiring a request removes the live row.

One transaction wins among answer, expiry, and cancellation. An identical retry by the recorded responder returns the durable answer without resuming twice. Other late writes return request_already_answered, request_expired, or request_canceled. A payload outside expect returns request_validation_failed with field details while the request stays pending. A schema-valid payload that names a missing annotated entity returns input_validation with origin: response and the exact nested field path; the request also stays pending.

Review an action before it runs

Add review to an action when its resolved parameters need admission before CompozyOS creates the task run. The request stores a bounded, redacted preview and keeps the exact proposal private until the decision commits.

- id: publish
  class: action
  kind: compozy__release_publish
  params:
    tag: "{{ .inputs.tag }}"
  produces:
    release_url: string
  review:
    when: inputs.environment == "production"
    prompt: "Review the production release"
    decisions: [approve, edit, reject, respond]
    responders:
      agents: deny
    on_reject:
      route: repair_release

approve admits the frozen proposal. edit validates and admits the replacement parameters. reject takes on_reject.route; without a route, the node fails with quality_rejection. respond validates the supplied value against the action's declared output shape and substitutes that result without running the action. The action timeout starts only after approve or edit.

Use compozy loop respond --decision <decision>. edit and respond require --payload; approve and reject do not. Fan-out reviews remain isolated by --item or item_index.

On this page