Skip to content

SDK API

The @palumb/sdk package (Bridge SDK) is the developer surface for writing palumb workflows. For the common case, this is the whole import:

import { workflow, BridgeClient, serveNest } from '@palumb/sdk/bridge';

It is Restate-free — your code is plain TypeScript behind one signed HTTP endpoint; palumb’s engine drives it.

Declares a workflow.

  • workflow(id, fn, options?)
  • id — the workflowId a trigger names. Must match ^([a-zA-Z]|_[a-zA-Z0-9])[a-zA-Z0-9_]*$ (letters/digits/underscore, no dots or hyphens). Invalid names throw up front.
  • fn({ payload, subscriber, step }) => Promise<void>. Runs on every step; completed steps are hydrated, not re-run, so keep non-step code cheap.
  • options.schedule (optional) — a cron expression (e.g. '0 9 * * 1') to schedule-trigger the workflow. Register it with POST /v1/sync.
  • options.payloadSchema (optional, reserved) — JSON Schema for the payload.
const orderCreated = workflow('orderCreated', async ({ payload, subscriber, step }) => {
await step.send('confirm', 'email', () => ({
subject: `Order ${payload.orderId} received`,
body: `<p>Thanks, ${subscriber?.subscriberId}!</p>`,
}));
});

Each step.* call is a durable step: its result is recorded by the engine and re-injected (never re-run) on subsequent invocations. Steps are serial — await them one at a time (parallel steps are a fast-follow).

Run arbitrary tenant code as a durable step. The returned JSON is recorded and replayed on later calls — the callback runs at most once per run. Make side effects idempotent.

const { token } = await step.run('mint', async () => ({ token: await mint() }));

step.send(stepId, channelKind, render, options?)

Section titled “step.send(stepId, channelKind, render, options?)”

Declare a delivery. channelKind is the kind (e.g. 'email'); the render callback returns { subject?, body }. palumb resolves your channel + the recipient and performs the physical send, idempotently. Returns SendResult ({ attemptId, status, providerMessageId? }).

options.to overrides the recipient: by default the send goes to the run’s subscriber; pass a different subscriber id to deliver to someone else (any subscriber of your tenant — palumb resolves it within the tenant). This lets one run notify several people, e.g. an escalation that alerts a supervisor.

await step.send('welcome', 'email', () => ({ subject: 'Welcome', body: '<p>Hi</p>' }));
// Deliver to a different subscriber than the run's:
await step.send('alertSupervisor', 'email', () => ({ subject: 'Escalation', body: '' }),
{ to: 'supervisor_1' });

Pause the run for a duration ({ seconds }) or until an instant ({ until }, an ISO string). No tenant code runs while waiting.

Suspend the run until an external event arrives (palumb resolves it via POST /v1/events/{runId}/signal), or the optional timeout elapses. Returns the event payload, or null on timeout. options is { event, timeout? } where timeout is seconds or a duration string.

const opened = await step.waitForEvent('awaitOpen', { event: 'email.opened', timeout: '3 days' });
if (!opened) await step.send('nudge', 'email', () => ({ subject: 'Still there?', body: '' }));

Collects your workflows. new BridgeClient().register([...]) returns the client (chainable); pass it to serveNest.

const bridge = new BridgeClient().register([orderCreated, onboarding]);

Builds an HTTP handler that answers palumb’s signed requests. Verify the HMAC against the raw body.

  • serveNest(bridge, { signingKey })signingKey is the secret palumb returned from PUT /v1/bridge.
  • Returns handle(req, res) — mount it on one route.
const handle = serveNest(bridge, { signingKey: process.env.PALUMB_SIGNING_KEY! });
const app = express();
app.use(express.json({
verify: (req, _res, buf) => { (req as any).rawBody = buf.toString('utf8'); },
}));
app.all('/api/palumb', (req, res) => handle(req as any, res as any));
TypeShape
WorkflowContext{ payload: Record<string, unknown>; subscriber?: Subscriber; step: StepTools }
WorkflowExecute(ctx: WorkflowContext) => Promise<void>
WorkflowOptions{ schedule?: string; payloadSchema?: object }
Subscriber{ subscriberId: string; … }
SendContent{ subject?: string; body: string }
SendResult{ attemptId: string; status: string; providerMessageId?: string }
DelayOptions{ seconds?: number; until?: string }
WaitForEventOptions{ event: string; timeout?: number | string }
WORKFLOW_ID_RE^([a-zA-Z]|_[a-zA-Z0-9])[a-zA-Z0-9_]*$