Human requests
Park a Loop at an ask node, inspect its redacted context, and admit one validated answer from a human or permitted agent.
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
| Action | CLI | HTTP or UDS | Native tool |
|---|---|---|---|
| List | compozy loop requests --state pending | GET /loop-requests?state=pending | compozy__loop_requests |
| Detail | compozy loop request --run-id <id> --generation <n> --node <id> | GET /loop-runs/:run_id/nodes/:node_id/request?generation=<n> | compozy__loop_request |
| Answer | compozy loop respond --run-id <id> --generation <n> --node <id> --payload '{"environment":"production"}' | POST /loop-runs/:run_id/nodes/:node_id/respond | compozy__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_releaseapprove 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.