Skip to content

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.

  • 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.

A digest is a palumb-hosted durable object, keyed per (digest, subscriber). You never host or serve it. Two moves:

  • Push an itemPOST /v1/digests/{digestId}/items with the target workflow, the subscriber, the item data, and the window. The first item of a batch arms a durable timer for windowSeconds; later items just append.
  • Flush — when the timer elapses, palumb runs your workflowId with the accumulated items as payload.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.

  1. 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>`,
    }));
    });
  2. 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
    }'
  3. palumb flushes once after the window: it runs orderDigest with payload.items = [{orderId:"O-1"}, …], and your step.send delivers one email.

  • 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.send is 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.

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.