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.
workflow()
Section titled “workflow()”Declares a workflow.
workflow(id, fn, options?)id— theworkflowIda 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 withPOST /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>`, }));});The step toolbox
Section titled “The step toolbox”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).
step.run(stepId, callback)
Section titled “step.run(stepId, callback)”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' });step.delay(stepId, options)
Section titled “step.delay(stepId, options)”Pause the run for a duration ({ seconds }) or until an instant ({ until }, an
ISO string). No tenant code runs while waiting.
step.waitForEvent(stepId, options)
Section titled “step.waitForEvent(stepId, options)”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: '…' }));BridgeClient
Section titled “BridgeClient”Collects your workflows. new BridgeClient().register([...]) returns the client
(chainable); pass it to serveNest.
const bridge = new BridgeClient().register([orderCreated, onboarding]);serveNest()
Section titled “serveNest()”Builds an HTTP handler that answers palumb’s signed requests. Verify the HMAC against the raw body.
serveNest(bridge, { signingKey })—signingKeyis the secret palumb returned fromPUT /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));| Type | Shape |
|---|---|
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_]*$ |