# @zapier/zapier-durable

Durable execution for the Zapier SDK. Pluggable adapter model for different runtimes.

> **Pre-1.0**: This package is published on the `latest` dist-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

```bash
pnpm add @zapier/zapier-durable
```

## Quick Start

```typescript
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

Durable runs are executed remotely on code-substrate-runner via the Zapier
SDK CLI, gated behind the experimental stability tier: `run-durable`,
`get-durable-run`, `list-durable-runs`, and `cancel-durable-run`. Opt in with
`--stability experimental`, or `ZAPIER_STABILITY=experimental` in the
environment. See the
[CLI reference](https://docs.zapier.com/sdk/cli-reference#next-gen-zaps-experimental)
for the full command list and options, or run `--help` for any command
(the opt-in is required even for `--help`, since these commands aren't
registered on the stable CLI surface):

```bash
npx zapier-sdk run-durable --help --stability experimental
```

## API

### `defineDurable`

Define a durable execution with optional input/output schemas.

```typescript
// Simple signature
defineDurable(name, run)

// Object signature with options
defineDurable({ name, run, description?, inputSchema?, outputSchema? })
```

**Options:**

- `inputSchema?: ZodSchema` - Optional Zod schema for input validation
- `outputSchema?: ZodSchema` - Optional Zod schema for output validation
- `description?: string` - Optional description for the durable

### `ctx.step`

Execute a durable step. Steps are memoized - they only execute once even if the execution replays.

```typescript
// Simple signature
await ctx.step(name, run)

// Object signature with options
await ctx.step({ name, run, outputSchema?, maxAttempts?, retryDelaySeconds? })
```

**Options:**

- `outputSchema?: ZodSchema` - Optional Zod schema for step output validation
- `maxAttempts?: number` - Optional max retry attempts on failure
- `retryDelaySeconds?: number` - Optional base delay between retries (backs off exponentially)

### `ctx.createCallback`

Create a callback that can be awaited later. Returns `[promise, callbackUrl]`.

```typescript
// 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:

```typescript
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.

```typescript
// 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"`)                                                       |
| `ZAPIER_DURABLE_WAIT_INPROCESS_THRESHOLD_SECONDS` | `0` (disabled)                                               | Waits at or below the remaining threshold at first await complete in-process instead of suspending |
| `ZAPIER_DURABLE_WAIT_INPROCESS_BUDGET_SECONDS`    | `120`                                                        | Cap on total in-process wait seconds per tick                                                      |

### 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_MILLISECONDS` | (none)  | Cap retry backoff delays (deprecated alias: `..._MS`) |

Example:

```bash
ZAPIER_DURABLE_TEST_MAX_WAIT_SECONDS=2 \
ZAPIER_DURABLE_TEST_MAX_RETRY_DELAY_MILLISECONDS=500 \
pnpm test
```
