Digest
A digest turns N items into 1 message. Instead of emailing a customer once per order, you accumulate their orders for a while and send a single summary. The hard parts — durable accumulation, a timer that survives restarts, and no double sends — are what the building block provides. You only render and send the batch.
When to use it
Section titled “When to use it”- Multiple events for the same subscriber arrive close together (orders, comments, mentions, alerts) and one combined message is friendlier than many.
- You want a fixed “settle” window before notifying (“wait a minute, then tell them what happened”).
If each event must notify immediately, you do not need a digest — fire a normal
trigger and step.send from
the workflow.
How it works
Section titled “How it works”A digest is a palumb-hosted durable object, keyed per (digest, subscriber).
You never host or serve it. Two moves:
- Push an item —
POST /v1/digests/{digestId}/itemswith the target workflow, the subscriber, the itemdata, and the window. The first item of a batch arms a durable timer forwindowSeconds; later items just append. - Flush — when the timer elapses, palumb runs your
workflowIdwith the accumulated items aspayload.items. Your workflow renders and sends one message.
sequenceDiagram
participant A as Your app
participant D as palumb digest (engine)
participant W as Your workflow (Bridge)
A->>D: POST /v1/digests/orders/items (O-1)
Note over D: batch=[O-1] · arm 60s timer
A->>D: POST …/items (O-2)
A->>D: POST …/items (O-3)
Note over D: window elapses (60s)
D->>W: run orderDigest({ items: [O-1, O-2, O-3] })
W->>D: step.send → one email
The batch and the timer live in palumb’s engine, durably — a restart mid-window resumes exactly where it left off.
Using it
Section titled “Using it”-
Write the flush workflow. It is a normal workflow that receives the batch as
payload.items:const orderDigest = workflow('orderDigest', async ({ payload, step }) => {const items = (payload.items as Array<{ orderId: string }>) ?? [];if (items.length === 0) return;await step.send('digest', 'email', () => ({subject: `${items.length} order(s) received`,body: `<p>Your recent orders:</p><ul>${items.map((i) => `<li>${i.orderId}</li>`).join('')}</ul>`,}));}); -
Push items as they happen — keyed by the subscriber, with a window:
Terminal window curl -sS "$PALUMB_BASE_URL/v1/digests/orders/items" \-H "Authorization: Bearer $PALUMB_API_KEY" \-H 'Content-Type: application/json' \-d '{"workflowId": "orderDigest","subscriberId": "cust_1","data": { "orderId": "O-1" },"windowSeconds": 60}' -
palumb flushes once after the window: it runs
orderDigestwithpayload.items = [{orderId:"O-1"}, …], and yourstep.senddelivers one email.
Guarantees and limits
Section titled “Guarantees and limits”- Exactly one flush per window. Only the first push arms the timer; subsequent pushes within the window append. After a flush the state clears, so the next push starts a fresh window.
- Per-key serialization. Concurrent pushes for the same
(digest, subscriber)are serialized — no lost updates. - No double sends.
step.sendis idempotent (see Delivery), so even if a flush is retried, the email is sent once. - Empty flush is a no-op. If the batch is empty when a flush runs, nothing is
sent (the workflow above guards on
items.length). - Fixed window, from the first item. The window measures from the first push of a batch — not a rolling/sliding window today.
See it run
Section titled “See it run”The Order digest example runs exactly this, end to end. Use a short window while you’re trying it out so you see the batch right away.
Reference
Section titled “Reference”- Durability & Restate — why the timer survives restarts.
- REST API › digests