@zapier/zapier-durable
v0.12.0
Published
Durable execution for Zapier SDK.
Readme
@zapier/zapier-durable
Durable execution for the Zapier SDK. Pluggable adapter model for different runtimes.
Pre-1.0: This package is published on the
latestdist-tag while the API stabilises. Expect breaking changes between minor versions until 1.0.
Features
- Clean ergonomics: Two consistent call styles everywhere (
(name, run)or({ name, run, ...options })) - Optional schemas: Input/payload validation with Zod, but schemas are always optional (defaults to
any) - Adapter-based: Pluggable for different runtimes
Installation
pnpm add @zapier/zapier-durableQuick Start
import { defineDurable } from "@zapier/zapier-durable";
import { z } from "zod";
const myDurable = defineDurable({
name: "my-durable",
description: "My durable example",
inputSchema: z.object({ userId: z.string() }),
run: async (ctx, input) => {
// Steps are durable - they only run once even if the execution replays
const result = await ctx.step("fetch-data", async () => {
const response = await fetch("https://api.example.com/data");
return response.json();
});
// Create a callback for external approval
const [approvalPromise, callbackUrl] = await ctx.createCallback({
name: "wait-for-approval",
payloadSchema: z.object({ approved: z.boolean() }),
});
// Send the callback URL (wrapped in a step so it only runs once)
await ctx.step("notify-approver", async () => {
await notifyApprover(callbackUrl);
});
// Wait for the callback to be delivered
const approval = await approvalPromise;
if (!approval.approved) {
throw new Error("Not approved");
}
return { approved: true, userId: input.userId };
},
});
const response = await myDurable({ userId: "123" });
// Technically, we'd wait for an approval callback, and resume with the execution ID:
// await myDurable(response.executionId)
// ...but imagine we had instant approval!
if (response.done) {
console.log(response.result);
// { approved: true, userId: "123" }
} else {
console.log(response.error);
// { message: "Not approved", name: "Error", stack?: string }
}CLI
$ npx zapier-sdk durable-run --help
Usage: zapier-sdk durable-run [options] <path>
Run a durable execution from a TypeScript/JavaScript file.
Arguments:
path Path to the durable file
Options:
--execution-id <string> Execution ID to resume (auto-generated if omitted)
--input <string> JSON input for the durable (e.g., '{"userId": "123"}')
--debug Enable verbose debug logging
--json Output raw JSON instead of formatted results
-h, --help display help for command$ npx zapier-sdk durable-callback --help
Usage: zapier-sdk durable-callback [options]
Deliver a payload to a waiting callback.
Options:
--callback-id <string> The callback ID to deliver to
--payload <string> JSON payload to deliver
--debug Enable verbose debug logging
--json Output raw JSON instead of formatted results
-h, --help display help for command$ npx zapier-sdk durable-status --help
Usage: zapier-sdk durable-status [options]
Show the status of a durable execution.
Options:
--execution-id <string> The execution ID to check
--debug Enable verbose debug logging
--json Output raw JSON instead of formatted results
-h, --help display help for commandAPI
defineDurable
Define a durable execution with optional input/output schemas.
// Simple signature
defineDurable(name, run)
// Object signature with options
defineDurable({ name, run, description?, inputSchema?, outputSchema? })Options:
inputSchema?: ZodSchema- Optional Zod schema for input validationdescription?: string- Optional description for the durable
ctx.step
Execute a durable step. Steps are memoized - they only execute once even if the execution replays.
// Simple signature
await ctx.step(name, run)
// Object signature with options
await ctx.step({ name, run, outputSchema? })Options:
outputSchema?: ZodSchema- Optional Zod schema for step output validation
ctx.createCallback
Create a callback that can be awaited later. Returns [promise, callbackUrl].
// Simple signature - awaiting resolves the delivered payload
const [promise, callbackUrl] = await ctx.createCallback("my-callback");
// With a payload schema (edge validation + typing), no deadline
const [promise, callbackUrl] = await ctx.createCallback({
name: "my-callback",
payloadSchema: z.object({ result: z.string() }),
});
// Send callbackUrl somewhere, then await the promise
const result = await promise;With a deadline (timeoutSeconds): the callback gets a server-enforced
expiry, and awaiting it resolves a CallbackResult - exactly one journaled
outcome, delivered in time or expired:
const [promise, callbackUrl] = await ctx.createCallback({
name: "approval",
timeoutSeconds: 24 * 60 * 60, // 24h SLA
payloadSchema: z.object({ approved: z.boolean() }),
});
const result = await promise;
if (result.status === "delivered") {
// result.value is the delivered payload, typed from payloadSchema;
// result.at is the server-journaled delivery time
} else {
// result.status === "expired": the deadline elapsed and result.at is
// the server-journaled deadline. The outcome is decided once — a late
// delivery to the callback URL cannot flip it back to delivered.
}The outcome is decided once by the server and read back from the journal on
replay - the SDK's clock never participates in deadlines. Without
timeoutSeconds the callback never expires and awaiting resolves the bare
payload, as in the simple signature.
Racing durable operations is unsupported. Do not use Promise.race
over steps, waits, or callbacks; express deadline patterns with
timeoutSeconds and branch on result.status instead. (Promise.all
over durable operations is supported.)
ctx.wait
Wait for a specified duration.
// Simple signature
await ctx.wait("delay-step", 60);
// Object signature
await ctx.wait({ name: "delay-step", seconds: 60 });Configuration
Configuration is via environment variables:
| Variable | Default | Description |
| ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------- |
| ZAPIER_DURABLE_ADAPTER | "filesystem" | Adapter type: "filesystem", "zapier", or "memory" |
| ZAPIER_DURABLE_FS_DIR | ~/.config/zapier-sdk/durable | Directory for filesystem adapter |
| ZAPIER_DURABLE_DETERMINISM_GUARD | "on" | Enable determinism checking ("on" or "off") |
| ZAPIER_DURABLE_API_URL | https://sdkapi.zapier.com/api/v0/sdk/code-substrate-runner | Base URL for the API (when adapter=zapier) |
| ZAPIER_DURABLE_API_JWT | (auto for localhost) | JWT token for API authentication |
| ZAPIER_DURABLE_DEBUG | "false" | Enable debug logging ("true" or "false") |
Test Mode (Integration Tests)
For faster integration tests, these env vars cap durations:
| Variable | Default | Description |
| ---------------------------------------- | ------- | -------------------------- |
| ZAPIER_DURABLE_TEST_MAX_WAIT_SECONDS | (none) | Cap ctx.wait() durations |
| ZAPIER_DURABLE_TEST_MAX_RETRY_DELAY_MS | (none) | Cap retry backoff delays |
Example:
ZAPIER_DURABLE_TEST_MAX_WAIT_SECONDS=2 \
ZAPIER_DURABLE_TEST_MAX_RETRY_DELAY_MS=500 \
pnpm test