resume
resume(map?: (exchange: Exchange<Current>) => ResumeRequest | Promise<ResumeRequest>): RouteBuilder<ResumeAcknowledgment>
Revive an exchange parked by .suspend() and run its continuation.
// Preferred: map the ingress exchange to the answer it carries.
craft()
.id('approval-replies')
.from(mail('INBOX'))
.authenticate(mailPrincipal)
.resume((ex) => ({
token: tokenFrom(ex.headers['routecraft.mail.subject']),
result: { approved: /^yes/i.test(ex.body.text ?? '') },
}))
.to(log())
// Fallback: the body is already shaped { token, result }.
craft().id('resume-api').from(http({ path: '/resume', method: 'POST' })).resume()
.resume() addresses an exchange, not a route. direct('x') names a route and enters it through its source; resume names one parked exchange and re-enters its pipeline partway down. That is what lets a mail-born exchange be continued by a chat-born answer: the original source takes no part in execution two, because sources create exchanges rather than revive them.
Any route ending in .resume() is a resume ingress: an HTTP webhook, a mail-reply parser, an ops CLI. There is no special resume transport.
The boundary
The mapping function owns shape: find the token, build the candidate answer. Only the ingress route knows what its transport looks like.
Revival owns validation: only the suspension knows the expect schema the suspending step declared, so the candidate answer is checked there, against the live schema read back off the route.
| Field | Type | Description |
|---|---|---|
token | string | The signed token minted when the exchange parked. |
result | unknown | The candidate answer, validated against the suspending step's expect. |
resumedBy | PrincipalRef | Who answered. Defaults to the ingress exchange's own principal, which is the value worth recording: it was verified live here. Set it explicitly only when the answerer is not the caller. |
What the ingress route receives
The revived route runs to completion before .resume() continues, so the acknowledgment it puts in the body reports how execution two actually ended, and the ingress route can answer the approver's own channel.
{
"status": "resumed", // or "duplicate"
"suspensionId": "3f1c…~0",
"routeId": "payout", // the suspended route, not this one
"outcome": { "status": "completed", "body": { "paid": true }, "at": "…" }
}
A duplicate answer (an approver double-clicks, a webhook is redelivered) returns the first one's cached terminal outcome with status: "duplicate" and re-runs nothing.
Authorizing the answerer
The token proves this deployment minted it. It does not prove its holder may answer. Authorizing the answerer is this route's job, and this route is where an authenticated principal is actually available:
craft()
.id('resume-api')
.authorize({ roles: ['approver'] })
.from(http({ path: '/resume', method: 'POST', auth: 'required' }))
.resume()
.to(log())
A principal that came back from the store with the parked exchange is marked restored and refused by authorize() with RC5043: it is a recorded shape with no live credential behind it. Re-verify after resume, or authorize here.
Revival failures
Each of these throws in the ingress route, so the answerer gets a typed error.
Three of them additionally re-enter the suspended route's error channel, so a route-scope .error() there can notify the approver and re-ask instead of leaving them at a dead link: RC5047, RC5048 and RC5050. Those are changes in the world the suspended route has to react to, and none of them is something a caller can provoke on demand.
RC5049 deliberately stays in the ingress route. A malformed answer is a per-request input error, the suspension stays resumable, and routing it through the suspended route would let anyone holding a token drive that route's re-ask path (approver notifications included) with junk. Shaping a reply to a bad answer belongs to the ingress route's own .error() handler, which is where the answerer's channel is.
| Code | Cause |
|---|---|
RC5041 | The token is malformed or its signature does not verify. |
RC5046 | The token verifies but the store holds no such suspension, or its route is not registered in this context. |
RC5047 | The suspension's ttl elapsed. |
RC5048 | The steps after the suspend point (or the expect schema) changed while the exchange was parked, so the stored approval no longer authorizes what would run. Refused before any of those steps execute. |
RC5049 | The answer does not satisfy expect. Ingress route only. The suspension stays resumable, so a corrected answer still works. |
RC5050 | The suspension was denied, typically because the run carrying it was cancelled. |
Related
.suspend()-- the other half..authorize()-- guarding the ingress.- Configuration → suspension -- the store and the signing secret.