Approvals
An Approval is a durable natural-language question an agent asks its user — over the user's preferred channel (SMS, Slack DM, email, etc.) — and gets an answer, potentially hours or days later.
Approvals are not internal safety gates. They're how an agent consults its user when it's about to take an action on the user's behalf and wants confirmation.
Example
An email arrives from Matt: "I can't make 9am tomorrow — can we do 8am instead?"
A trigger wakes the agent, which reasons: "I shouldn't move a calendar event without checking." The agent calls the ask_my_user tool with the full context as the question:
"Matt can't meet at 9am tomorrow, he wants to meet at 8am — is that okay?"
The approval is dispatched via Sendblue to the user's phone. The agent's session goes idle.
Hours later, the user replies "yes" on iMessage. The inbound Sendblue webhook routes the reply to the approval. The workflow wakes up, resumes the session with the answer injected, and emails Matt back to confirm 8am.
Key principle: the approval message must contain enough context for the user to decide without re-asking the agent. "Okay to reschedule?" is bad. "Matt can't meet at 9am tomorrow, he wants to meet at 8am — is that okay?" is good.
Three ways to create an approval
1. ask_my_user tool (primary, inside a session)
Agents call this tool mid-session when they need confirmation. The tool:
- Creates a durable
approvalrow. - Dispatches it via the agent's
userChannels. - Blocks the session with a polling loop until the user answers or it times out.
- Returns the user's choice back to the LLM.
"Matt can't meet at 9am tomorrow, he wants to meet at 8am — is that okay? [yes / no / suggest another time]"2. Workflow approval node (declarative)
For fixed gates in a multi-step workflow:
jsonc
{
"type": "approval",
"question": "Ship build {{version}} to production?",
"options": ["yes", "no"],
"next": { "yes": "deploy", "no": "rollback" },
}3. SDK client.askHuman(...) (standalone)
For one-off confirmations with no workflow:
typescript
const h = await client.askHuman({
question: "Should I reschedule your 3pm?",
options: ["yes", "no"],
expiresInMs: 30 * 60 * 1000,
});
const answer = await h.waitForAnswer();User channels
Each agent declares userChannels in swarmlord.jsonc — the channels the agent can use to reach its user:
jsonc
{
"userChannels": [
{
"provider": "sendblue",
"config": {
"api_key": "{{secrets.SENDBLUE_API_KEY}}",
"api_secret": "{{secrets.SENDBLUE_API_SECRET}}",
"from": "{{secrets.SENDBLUE_FROM}}",
"to": "{{secrets.USER_PHONE}}",
},
"answerVia": "reply",
},
],
}Channels are used in declared order. answerVia: "reply" tells the dispatcher to format the message for an in-channel reply; "signed-url" embeds one-click answer links.
Answering
An approval can be answered from any channel:
- Signed URL (public, HMAC-validated): a GET or POST to
/approval/:token/answer?choice=yes. One-click. - Dashboard / CLI (authenticated):
POST /approval/:id/answerwith{ choice, note }. - Inbound webhook reply (threaded): when the user replies on SMS or Slack, the trigger provider's
matchApprovalReply()matches the thread and routes the reply to the approval.
Answers are idempotent — a second answer to an already-answered approval is rejected.
Durability
Approvals are durable and independent. They don't live inside the session or workflow that created them — the question persists until it's answered, expires, or is cancelled. An agent (or the user's reply) can answer an approval days after it was asked, long after the session that asked is gone.
See also
- Workflows
- Triggers & Integrations — how inbound channel replies are matched