Skip to main content
A due date passing, a price expiring, a temporary hold ending: none of these changes a row by itself. Something has to commit when the moment arrives. A durable wait is how your program says what that is. You declare a wait with two of your own internal functions: a query that says when the next boundary is, and a mutation that does the work at it.
bijection/invoices.ts
The engine wakes at exactly the instant nextDueDate returned and schedules markOverdue. Nothing in this file mentions an interval.

How a wait works

  • due is a read, not a poll. It is an internal query that returns the next boundary in epoch milliseconds, or null when there is none. The rows it reads become the wait’s dependencies, so a committed write that changes its answer, such as a new invoice or a paid one, is what re-arms the wait. Between boundaries nothing runs.
  • then runs through the scheduler. When the boundary is reached, the engine schedules then, an internal mutation, in the same transaction that settles the wait. It then runs like any scheduled mutation, with the same retry behavior.
  • The wait survives restarts. It is stored in the database, like a scheduled function.
Write then so it checks what is actually due in its own transaction, as markOverdue does, rather than trusting the instant that woke it. Another write may have changed the data in between.
Compared with a cron job that sweeps every minute, a wait runs only when something is due, and its precision is the boundary itself rather than the cron’s interval.

Declaring a wait

Pass defineWait an object with: Each field takes a function reference, such as internal.invoices.nextDueDate, or its "module:function" name. Export the declaration from any module in your bijection/ folder. due and then must be different functions, and keys must be a third one. When you deploy, each name must resolve to a function your deployment declares, of the right kind, and internal. A deployment that names a missing or public function is refused. then is resolved again when the boundary arrives, against the code deployed then. If a later deployment removed it, made it public or changed its kind, the release is refused with WaitContinuationWithdrawn and the wait stays held rather than running work you no longer declare.

One wait per key

A single declaration keeps one standing wait. To give each item its own boundary, add a keys query. It returns a list of stable string keys, and each key becomes its own wait: its due runs in its own transaction and receives { key }, and so does then.
bijection/holds.ts
  • keys returns at most 32 keys, each 1 to 128 bytes. A deployment holds at most 1024 standing waits in total.
  • One key’s reads never become another key’s dependencies, and a key whose due throws does not delay the others.
  • When keys stops listing a key, that key’s wait is retired. If keys itself throws, nothing is retired: a failure to list is not evidence that the items are gone.
  • keys should read routing only, never the protected data the waits guard. then still rechecks the item, because a key can be removed between the boundary and the continuation.

Limits

  • A boundary is a JavaScript millisecond timestamp.
  • The only condition a wait can express is “this instant has been reached”.
  • A wait cannot be created at run time. Every wait comes from a deployed defineWait declaration, so a program cannot register a condition or a continuation the deployment does not declare.

Source barriers

Source barriers are in beta.
A decision that reads data synced from an external source sometimes needs that data to be current or complete first. A source barrier states that requirement, and the read is refused when it does not hold, instead of returning a stale answer as if it were current. You pass a barrier to ctx.sources.coverage, which reports what the engine can prove about one installed source:
bijection/payouts.ts
The barriers are: When the barrier is not satisfied, the call throws an error with code SourceBarrierUnsatisfied. The check runs in the same transaction as the reads that follow it, and the coverage read is tracked like any other read, so a query that was refused is re-evaluated when the source publishes. Read on about synced sources in Integrations.