Continuous Mode
By default, tasks run in linear mode — each node processes a single message and passes its result to the next node. Continuous mode changes this: service nodes and logic nodes can yield multiple messages over time, and the task stays alive to process each one.
When To Use It
Section titled “When To Use It”Continuous mode is designed for workflows that react to a stream of events rather than a single input:
- An email listener that emits a message for every new email
- A file watcher that emits when files appear in a directory
- A webhook receiver that forwards incoming payloads
- A data pipeline that processes records one by one from a large source
- A logic node that fans out work by yielding multiple intermediate results
How It Works
Section titled “How It Works”1. Use Generator Handlers
Section titled “1. Use Generator Handlers”In continuous mode, nodes use yield to emit values instead of return. Each yield pushes a new message through the downstream nodes in the task template.
Service nodes:
createService('1.0.0', async function* (ctx) { for await (const email of watchInbox(ctx.configuration.folder)) { yield { subject: email.subject, from: email.from } }})def handler(ctx): for email in watch_inbox(ctx.configuration["folder"]): yield {"subject": email.subject, "from": email.sender}
Service("1.0.0", handler)Logic nodes can also yield multiple times. This is useful for fanning out data or generating intermediate steps:
// Logic node: emit one message per item in the input arrayfor (const item of context.input.items) { yield { processed: transform(item) }}2. Set the Task Template to Continuous Mode
Section titled “2. Set the Task Template to Continuous Mode”In the task template settings, set the mode to Continuous. This tells Requence that nodes are allowed to emit multiple messages and the task should not exit after the first result.
Stopping Cleanly
Section titled “Stopping Cleanly”When a task is stopped — via the UI, the API, or an exit node — Requence signals the running generator to stop. If your generator is suspended in a long await (for example, sleeping until the next scheduled tick), it will not exit until that await resolves naturally.
To exit immediately, pass ctx.terminated.signal to any abort-aware API:
import { setTimeout } from 'node:timers/promises'
// A cron-style emitter: fires on schedule, exits immediately when stoppedcreateService('1.0.0', async function* (ctx) { while (true) { const delay = getNextTickDelay(ctx.configuration.expression) await setTimeout(delay, null, { signal: ctx.terminated.signal }) yield { firedAt: new Date().toISOString() } }})ctx.terminated.signal is a native AbortSignal that fires the moment the task is stopped. It works with any API that accepts a signal: fetch, setTimeout, database clients, and so on. The framework automatically handles the resulting AbortError — no try/catch is needed in your generator.
When none of your awaits support AbortSignal, you can race ctx.terminated as a plain Promise instead:
const result = await Promise.race([ waitForExternalEvent(), ctx.terminated,])See Context API — ctx.terminated for the full reference.
Crash Recovery
Section titled “Crash Recovery”If a continuous service disconnects while running, Requence automatically detects the disconnection and re-dispatches the original message so a new service instance can pick up the work. This happens transparently — the task continues without manual intervention.
The node’s computed configuration fields run again on that reconnect, and the reconnecting service is given what they produce. That is what lets a cursor field resume from where the last run reached instead of from where the task started — see The store. A field whose function reads only the node’s input produces the same value as before, so nothing changes for it; a node with no computed fields is re-dispatched exactly as it was.
Aggregating Results
Section titled “Aggregating Results”Since nodes can emit multiple values, you often need a downstream logic node to aggregate or filter them. A logic node is activated once per upstream yield, so anything it wants to carry from one activation to the next has to be remembered — that is what the store is for.
Use private with the task binding: it is this node’s own, remembered for the rest of this task, and it goes when the task does — exactly the lifetime of an aggregation. (task is also the default, so the argument could be left off; it is written out here because the line reads better with both axes visible.)
// Logic node: collect the subjects yielded so far and exit after 10const subjects = context.store.private<string[]>('subjects', 'task')const collected = [ ...((await subjects.get()) ?? []), context.getNodeData('email-listener').subject,]
if (collected.length >= 10) { return collected}
await subjects.set(collected)Two things this pattern depends on:
-
Turn the node’s Concurrency setting off. By default every activation starts immediately, so two of them can read the same list and each write their own — losing one. With concurrency off, the next activation waits for the previous one to finish. The store does not arbitrate this: the last write wins.
The switch makes a node wait for itself, and that is all it does. It is the answer for a node racing its own activations — this pattern — and it is not an answer for a
sharedname that two different nodes write, which has no protection at all. Keep an accumulatorprivate. -
Handle the first activation. Nothing has been written yet, so the first read answers nothing (
?? []above). See The Store for why the empty case is part of the type.
Keep what you accumulate small — 64 KiB per value, and 64 names per slot group. Collecting an unbounded stream of records is a database’s job, not this one’s.
Running From The Editor Shares The Template’s Memory
Section titled “Running From The Editor Shares The Template’s Memory”Pressing Run in the editor starts a real task, and a real task uses the saved template’s stored data. There is no sandbox copy.
That matters most for the taskTemplate binding — a cursor, a “last seen” marker — because two things are true at once:
- What a test run writes stays behind. The next scheduled run reads it, and it keeps reading it after you close the editor.
- The draft you pressed Run on is not the program that data was written by. You may have changed the script, the node, or what the value means; the slot does not know that, and nothing clears it, because clearing happens when a template is saved and a draft run saves nothing.
This is deliberate rather than an oversight. The alternative — giving a draft run its own private slots — sounds safer and is worse in practice: an email node with a fresh cursor starts at zero, re-downloads a fortnight of mail and fires every downstream side effect during your test, and it makes the one thing you were trying to test, the cursor’s behaviour, untestable.
So the run dialog states it when the template’s nodes use the store, and the tools are the ordinary ones: clear the node’s stored data before or after a test, and keep in mind that a run that is still going locks clearing until it is stopped.
The dialog is deliberately over-eager: it appears on any mention of context.store in a script, without reading which binding is used. A template that only ever keeps values for the length of one task therefore sees it too, and for that template nothing stays behind. It is worded as a condition rather than a fact for exactly that reason — being wrong the other way round would let a test run move a production cursor with nothing said.
Callbacks
Section titled “Callbacks”Since nodes stay alive after yielding a message, continuous mode enables bidirectional communication via callbacks. A service can yield a callback object as part of its output, pause execution, and wait for a downstream node to invoke it with a response — then continue based on that value.
That waiting is what needs continuous mode. A service can also emit a durable callback, which nothing waits on and which any instance of the service can run later — see Callbacks for the difference and when a template needs which.
Max Node Executions
Section titled “Max Node Executions”In continuous mode, the Max node executions setting acts as a per-second rate limit rather than a total count. If more than the configured number of node executions occur within a single second, the task is aborted. This protects against infinite loops (e.g. a logic node feeding back into itself) while allowing long-running tasks to process data indefinitely at a normal pace.
The default value is 10 executions per second. Increase it for high-throughput workflows.
Limitations
Section titled “Limitations”- Generator handlers for services — continuous services require the handler to be a generator function. Regular function handlers always produce a single result.
- No exit required — continuous task templates may not have exit nodes if they are designed to run indefinitely. They can be stopped via the API or the Requence UI.
- Stateless recovery — when a service is recovered after a crash, it receives the original input and configuration, not the position it had reached. A service that has to resume from where it left off must have written that position down as it went — see The Store.