Callbacks
Callbacks let a downstream node send data back to an upstream service. This enables bidirectional communication within a task — a service can emit a value together with a function, and a later node decides whether to call it.
Two Kinds, and the Difference Matters
Section titled “Two Kinds, and the Difference Matters”A callback comes in two kinds, and the service author picks which one when they write it:
| Survives a redeploy, restart or scale event? | |
|---|---|
| Durable | Yes. Any instance of the service can run it — including one started after the emitting instance is gone. |
| Connection-bound | No. It lives only in the process that emitted it, and only until that message finishes. |
This is a template design decision, not just a service detail:
- A callback that is used immediately — a logic node right after the service picks one of the options it just offered — can be either kind.
- A callback the graph may call later — after a wait, after a human decides, after a node that only runs tomorrow — must be the durable kind. A connection-bound one is dead by then, and nothing tells the graph: calling it looks exactly like calling a live one, and simply nothing happens on the other side.
If a template hands a callback to a node that may not run for a while, check with whoever owns the service that it is the durable kind.
A durable callback that nobody can run — the service was scaled to zero, the version was deleted, or a colleague stopped their local dev process — is reported on the node in the task run, saying which of those it was. It waits about a minute for an instance to pick it up, so an ordinary redeploy is invisible.
When To Use Them
Section titled “When To Use Them”Callbacks are useful when a service needs input from a downstream node before it can proceed, or when it wants to offer the graph an action on the value it just emitted:
- A service generates options → a logic node selects one → the service continues with the selection
- A service requests approval → a downstream step decides → the service acts on the decision
- A service emits a mail → a logic node decides it is spam → it calls
delete()on that mail
The first two wait for an answer, so they are connection-bound. The third does not, so it should be durable.
How It Works
Section titled “How It Works”1. The Service Creates and Yields a Callback
Section titled “1. The Service Creates and Yields a Callback”A connection-bound callback, which the service then waits on:
import { createService, createCallback } from '@requence/service'
createService('1.0.0', async function* (ctx) { const callback = createCallback<number>()
// Yield the callback as part of the output yield { chooseAmount: callback }
// Wait for the downstream node to respond const amount = await callback.response()
// Continue with the received value yield ctx.toOutput('result', { total: amount * 10 })})from requence.service import Servicefrom requence.requence_callback import Callback
def handler(ctx): callback = Callback()
# Yield the callback as part of the output yield {"chooseAmount": callback}
# Wait for the downstream node to respond amount = callback.response()
# Continue with the received value yield ctx.to_output("result", {"total": amount * 10})
Service("1.0.0", handler)A durable callback, declared once and given this emission’s data:
import { createDurableCallback, createService } from '@requence/service'
// Declared once, outside the handler — that is what makes it durable.const deleteMail = createDurableCallback<null, { uid: number }, '1.0.0'>( 'deleteMail', async (_arg, { uid, configuration }) => { await deleteFromMailbox(configuration, uid) },)
createService('1.0.0', async function* () { for await (const mail of mails) { yield { subject: mail.subject, delete: deleteMail.for({ uid: mail.uid }) } }})from requence.service import Service, create_durable_callback
# Declared once, outside the handler — that is what makes it durable.delete_mail = create_durable_callback( "deleteMail", lambda _arg, context: delete_from_mailbox( context["configuration"], context["uid"] ),)
def handler(ctx): for mail in mails: yield { "subject": mail.subject, "delete": delete_mail.for_({"uid": mail.uid}), }
Service("1.0.0", handler)2. A Downstream Node Invokes the Callback
Section titled “2. A Downstream Node Invokes the Callback”The callback arrives as a callable function on the downstream node’s input, and it looks the same whichever kind it is. Calling it sends the value back:
// Logic node scriptawait context.input.chooseAmount(42)# Logic node scriptawait context.input["chooseAmount"](42)In a logic node the call is asynchronous: it crosses the sandbox boundary, and what you
await is the dispatch, not an answer — a callback never returns one. The await is
optional, since the node cannot finish while an invocation is still in flight, and it buys
ordering rather than error handling: a failed dispatch fails the node, it does not throw at
your line. In a service, a reconstructed callback is a plain call with nothing to await.
This holds in both logic-node languages, and it holds for a callback you minted yourself
with createCallback / create_callback — calling that one resolves locally, so its await
returns at once rather than waiting for anything.
A callback that carries no value is called with nothing:
if (context.input.subject.startsWith('Spam')) { await context.input.delete()}if context.input["subject"].startswith("Spam"): await context.input["delete"]()3. The Service Receives the Response
Section titled “3. The Service Receives the Response”For a connection-bound callback, callback.response() resolves with the value (42 in this example). The service can then continue its execution and yield further results.
A durable callback has no waiting handler to resolve. Requence delivers the invocation to the service version, which runs the declared function — this instance, or whichever one is free.
Example
Section titled “Example”A complete task template using callbacks might look like this:
- Entry → Service (generator, yields a callback) → Logic (invokes callback) → Exit
- The service also has a named output
resultthat connects directly to the exit
// Service handlercreateService('1.0.0', async function* (ctx) { const callback = createCallback<number>() yield { multiply: callback }
const factor = await callback.response() yield ctx.toOutput('done', { result: factor * 100 })})// Logic node scriptcontext.input.multiply(10)The task completes with { result: 1000 }.
Multiple Callbacks
Section titled “Multiple Callbacks”A service can create and yield multiple callbacks, either sequentially or in the same yield, and they need not be the same kind:
createService('1.0.0', async function* (ctx) { const approve = createCallback<boolean>() const setAmount = createCallback<number>()
yield { approve, setAmount }
const [approved, amount] = await Promise.all([ approve.response(), setAmount.response(), ])
if (approved) { yield ctx.toOutput('confirmed', { amount }) }})createCallback is also available as a global inside logic nodes — see Logic Nodes. A logic node’s own callback is always connection-bound; only a service can declare a durable one.