Skip to content

Human waits and external callbacks

A wait records what a run is waiting for, a correlation token, deadline and kind. It is durable state, not a sleeping thread. INPUT, AUTHORIZATION, EXTERNAL, TIMER and RECONCILIATION describe the wait’s purpose. The host remains responsible for asking the human, receiving the callback or evaluating a timer.

Correlated waits and an ALL join · Executable control simulation

import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';
// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
"id": "c",
"missionId": "m",
"aiwsEdition": "0.4",
"profile": "finite-v1",
"budget": "100",
"deadline": "10000",
"criteria": [
"review"
],
"actions": [
"write"
],
"resources": [
"doc"
],
"requiresApproval": false,
"maxAttempts": "2",
"maxAuthorizationAgeMs": "10",
"features": [
"triggers",
"graphs"
]
}));
let state = createMission(contract);
// Step 1
state = reduce(state, parseCommand(JSON.stringify({
"type": "startRun",
"runId": "r"
})), '10');
// Step 2
state = reduce(state, parseCommand(JSON.stringify({
"type": "wait",
"runId": "r",
"waitId": "w",
"correlation": "corr",
"deadline": "100",
"kind": "INPUT"
})), '10');
// Step 3
state = reduce(state, parseCommand(JSON.stringify({
"type": "transition",
"runId": "r",
"to": "WAITING"
})), '10');
// Step 4
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
"type": "resume",
"waitId": "w",
"correlation": "wrong"
})), '10'), (error: any) => error.code === 'CORRELATION_MISMATCH');
// Step 5
state = reduce(state, parseCommand(JSON.stringify({
"type": "resume",
"waitId": "w",
"correlation": "corr"
})), '10');
// Step 6
state = reduce(state, parseCommand(JSON.stringify({
"type": "join",
"runId": "r",
"waitIds": [
"w"
],
"mode": "ALL"
})), '10');
assert.deepEqual(state["runs"]["r"]["state"], "READY");
console.log('PASS: waits');

Creating a wait does not automatically transition the run to WAITING. The transition requires at least one pending wait and no unsettled attempts. resume satisfies a wait with the matching token; join moves a WAITING run to READY when the required waits are satisfied. ANY cancels unselected pending waits. These operations do not automatically dispatch subsequent work.

Correlation tokens should be opaque and scoped to the intended decision. Do not expose them as proof of identity. Authenticate the callback principal, check its mission/wait association and verify that its content is appropriate before allowing satisfaction. A replayed or late callback must not apply to a new wait with unrelated material.

Satisfy one wait with a correlated event · Executable control simulation

import assert from 'node:assert/strict';
import { createMission, parseContract, parseCommand, reduce } from '@aiws/sdk';
// Trusted simulation: this example makes no external calls.
const contract = parseContract(JSON.stringify({
"id": "c",
"missionId": "m",
"aiwsEdition": "0.4",
"profile": "finite-v1",
"budget": "100",
"deadline": "10000",
"criteria": [
"review"
],
"actions": [
"write"
],
"resources": [
"doc"
],
"requiresApproval": false,
"maxAttempts": "2",
"maxAuthorizationAgeMs": "10",
"features": [
"triggers",
"graphs"
]
}));
let state = createMission(contract);
// Step 1
state = reduce(state, parseCommand(JSON.stringify({
"type": "startRun",
"runId": "r"
})), '10');
// Step 2
state = reduce(state, parseCommand(JSON.stringify({
"type": "wait",
"runId": "r",
"waitId": "w",
"correlation": "corr",
"deadline": "100",
"kind": "INPUT"
})), '10');
// Step 3
state = reduce(state, parseCommand(JSON.stringify({
"type": "registerTrigger",
"trigger": {
"id": "t",
"revision": "1",
"kind": "EXTERNAL_RESPONSE",
"source": "trusted",
"eventType": "doc.changed",
"contractId": "c",
"action": "RESUME_WAIT",
"enabled": true,
"notBefore": "0",
"expiresAt": "10000",
"maxAgeMs": "100",
"maxConcurrent": "2",
"rateLimit": "10",
"rateWindowMs": "100",
"maxDepth": "3",
"waitId": "w"
}
})), '10');
// Step 4
assert.throws(() => reduce(state, parseCommand(JSON.stringify({
"type": "fireTrigger",
"triggerId": "t",
"runId": "r",
"event": {
"id": "e",
"source": "trusted",
"type": "doc.changed",
"occurredAt": "10",
"data": {
"flag": true
},
"depth": "0"
}
})), '10'), (error: any) => error.code === 'WAIT_CORRELATION');
// Step 5
state = reduce(state, parseCommand(JSON.stringify({
"type": "fireTrigger",
"triggerId": "t",
"runId": "r",
"event": {
"id": "e",
"source": "trusted",
"type": "doc.changed",
"occurredAt": "10",
"data": {
"flag": true
},
"depth": "0",
"correlation": "corr"
}
})), '10');
assert.deepEqual(state["waits"]["w"]["status"], "SATISFIED");
console.log('PASS: external-response');

The trigger must target the same run and a pending, unexpired wait, and the event must carry the matching correlation. Generic resume and trigger-based resume have distinct duplicate paths; application tests should exercise the API actually used by the integration.

An AUTHORIZATION wait and an action Approval are different records. Satisfying the wait tells the application a response arrived. It does not create the material-bound approval used at effect dispatch. The application must authenticate, interpret and record that approval separately. Similarly, satisfying a verification wait does not itself establish PASS.

If a decision arrives after deadline or after material changes, preserve the receipt in the application audit trail and request an appropriate new decision. Do not bypass WAIT_EXPIRED by rewriting historical timestamps. If the human rejects a plan, use the workflow’s authorized rejection/disposition path instead of pretending approval was never requested.

The SDK can record run PAUSED or mission SUSPENDED states. It cannot freeze an external process, stop provider billing or revoke an already-submitted request automatically. Engine-level pausing will distinguish preventing new work, requesting a checkpoint, and confirmed stopped work. Wake-up and automatic restart policies remain host/engine responsibilities.