approvals/deploy workflow from Ask for input, and a supervisor with canAnswer: true that owns session:approvals (Write a rules supervisor). Server settings are yukta.input.*; restart the server after changing them. Add -H "X-API-Key: $YUKTA_API_KEY" to every curl if your server has keys.
By default a request has one tier that any supervisor or operator may answer, with no timeout. A route puts several tiers in a row. A tier that times out, or that someone hands the request up from, passes it to the next; the last tier applies the route’s on-final-timeout (FAIL or USE_DEFAULT) and then the request’s own onTimeout.
Steps
1
Define the route
A tier has
responders (who is asked), timeout, mode, and for supervisors min-confidence. A route named default applies to every request that names no route.A route is checked when the server starts: no tier, a non-positive timeout, a quorum under 2, or a last tier that is neither
HUMAN nor given an on-final-timeout stops it with a message. A tier with nobody to ask is skipped at once.2
Choose how a tier decides
mode is FIRST (the first valid answer wins, the default), QUORUM (quorum, at least 2, equal votes win) or UNANIMOUS. In a vote each supervisor votes once, the request is answered when enough agree and the rationale names the voters; a split vote hands the request up. A person’s answer always decides at once and is never a vote. min-confidence makes a supervisor answer below it advice for the next tier, not an answer.3
Pick the route for a request
A request picks its route in this order: the
route option of the asking node (ASK_INPUT, WAIT_FOR_SIGNAL), the input.route tag of its session, yukta.input.routes.default, then the built-in one-tier route. A route name nobody configured is refused. For a model’s questions, the node’s inputRoute sets it.4
Add an outside responder
A tier can call a chat bot, a ticket system or another agent when a request reaches it. Write it as The server posts JSON with
external:webhook:<name> and configure the target:requestId, executionId, sessionId, workflowId, kind, prompt, context, options, risk, tier, advice, deadline, answerUrl, token and tokenExpires. The prompt and context are the redacted ones; an answer is never in it. With a signing-secret, the header X-Yukta-Signature: sha256=<hex HMAC-SHA256 of the body> lets the receiver check the sender. The URL must pass the egress rules (yukta.egress.*); redirects are not followed.Delivery is at least once: a failed post is retried after 1 s, then twice as long each time up to 5 minutes, and given up after 10 tries. The receiver must treat a repeated requestId as the same request. Only the leader delivers, and queued deliveries are lost on a restart unless requests are stored in PostgreSQL (Plus).5
Answer with the token
The receiver answers without an API key, once, until The token is bound to the request and the tier and does not fix the decision. It is a bearer credential: anyone who holds it can answer, so use HTTPS only and set
tokenExpires:yukta.input.token-secret to a long random value (without it, tokens stop working after a restart). Every refusal (unknown request, wrong tier, bad, expired or used token) is the same 403 Invalid or expired token; an address with 30 refused calls in a minute, or a request with 10, gets 429. The answer goes through the same review and audit as any other, as actor token:<tier>. A HIGH request can be answered this way only when the tier is type: HUMAN.6
Or let an agent pull
An external agent that cannot be called lists open requests (
GET /api/inputs, MCP list_pending_inputs), follows GET /api/events/stream?type=InputRequested,InputEscalated, and answers with POST /api/executions/{executionId}/inputs/{requestId}/answer and an OPERATOR key. Example 15 is a curl script that does it. Such a key counts as a person for the risk rule.Verify it worked
Start thedeploy workflow with "environment":"staging" and a LOW request, then:
ANSWERED within the auto tier. Change the payload so the rule does not match: the supervisor escalates, or 30 seconds pass, and the request moves to the ops tier. The request keeps the way it came: asked lists each hand-up ({who, said, reason, at}, newest 20), and the Inputs panel shows it as the approval chain. GET /api/supervisors/decisions shows a refused answer as rejected with the rule that failed (audience, tier, risk, scope or separation), counted in yukta.input.denied{rule}, and the request stays open for the next responder. For a webhook, yukta.input.delivery{responder,outcome} and the log entries INPUT_DELIVERED and INPUT_DELIVERY_FAILED show whether it was sent.
Variations
- Limits.
yukta.input.default-ttl1h,max-ttl24h,max-open-per-execution5,max-open-per-session50. At the deadlineFAILfails the asking node andUSE_DEFAULTanswers with the default decision as the system. - A request raised by a supervisor’s own run goes only to people and to supervisors that strictly cover the starter’s scope, so it never waits for itself.
- Redaction. Secrets in the prompt and context (key-like values,
${env.X}, andyukta.input.redact-keys) are replaced before the request is stored, published or logged. Treat what is left as data. - After a restart. In-memory requests are lost with their runs. With Plus and
yukta.input.store-type=postgres, open requests, tiers, votes and queued deliveries survive, and a resumed run takes over its open request: Persist sessions.
Related
- Who decides for the risk rules the server enforces.
- Configuration for every
yukta.input.*setting. - Ask for input, Build a hierarchy.