# Durable Function APIs

Durable functions are Rippling functions that receive a `DurableFunctionContext`. They let you split work into named, durable steps; pause execution; wait for external events; and fail runs with structured metadata.

## Function signature

```ts
import RipplingSDK, { DurableFunctionContext, FunctionEvent, FunctionResponse } from '@rippling/rippling-sdk';

export async function onRipplingEvent(
  event: FunctionEvent,
  context: DurableFunctionContext,
): Promise<FunctionResponse> {
  return new FunctionResponse({ statusCode: 200 });
}
```

## Context APIs

```ts
// context.step()
const documentResult = await context.step('send offer letter', async () => {
  return await client.workflowActionExecutions.create({
    action_type: 'send_document',
    payload: {
      document: offerLetterDocumentId,
      recipients: [employeeId],
    },
    workflow_definition_id: event['workflow_definition_id'],
  });
});

// context.sleep()
await context.sleep('wait before retry', '1 hour');
await context.sleep('wait 5 seconds', 5000); // milliseconds

// context.sleepUntil()
await context.sleepUntil('wait until launch', new Date('2025-01-01T00:00:00Z'));
await context.sleepUntil('wait until timestamp', Date.parse('2025-01-01T00:00:00Z'));

// context.waitForEvent()
const approval = await context.waitForEvent<{ approved: boolean }>('wait for approval', {
  type: 'approval',
  timeout: '24 hours',
});

if (approval.isTimeout()) {
  context.fail('Approval timed out', {
    code: 'approval_timeout',
    details: approval,
  });
}

if (!approval.payload.approved) {
  context.fail('Approval was rejected', {
    code: 'approval_rejected',
    details: approval.payload,
  });
}
```

## `DurableFunctionContext`

`DurableFunctionContext` extends the standard `FunctionContext` with durable execution methods.

```ts
interface DurableFunctionContext extends FunctionContext {
  step<T>(name: string, fn: () => Promise<T> | T, opts?: unknown): Promise<T>;
  sleep(name: string, duration: DurableDuration): Promise<void>;
  sleepUntil(name: string, timestamp: DurableTimestamp): Promise<void>;
  waitForEvent<T = unknown>(
    name: string,
    opts: DurableWaitForEventOptions,
  ): Promise<DurableWaitForEventResult<T>>;
  fail(message: string, opts?: DurableFailureOptions): never;
}

interface DurableWaitForEventEventResult<T = unknown> {
  kind: 'event';
  waitName: string;
  eventType: string;
  payload: T;
}

interface DurableWaitForEventTimeoutResult {
  kind: 'timeout';
  waitName: string;
  eventType: string;
}

interface DurableWaitForEventResultHelpers<T = unknown> {
  isEvent(): this is DurableWaitForEventEventResult<T> & DurableWaitForEventResultHelpers<T>;
  isTimeout(): this is DurableWaitForEventTimeoutResult & DurableWaitForEventResultHelpers<T>;
  throwTimeout(this: DurableWaitForEventTimeoutResult & DurableWaitForEventResultHelpers<T>): never;
}

type DurableWaitForEventResult<T = unknown> =
  | (DurableWaitForEventEventResult<T> & DurableWaitForEventResultHelpers<T>)
  | (DurableWaitForEventTimeoutResult & DurableWaitForEventResultHelpers<T>);
```

The inherited `FunctionContext` includes environment, function metadata, and settings.

```ts
interface FunctionContext {
  env: {
    rippling_user_bearer_token: string | undefined;
  };
  function: {
    company_id: string;
    function_id: string;
    function_version_id: string;
    run_id: string;
    role_id?: string;
  };
  settings: Record<string, string | boolean | number>;
}
```

## Step API

Use `context.step()` for work that should be durably tracked. Each step should have a stable, descriptive name.

```ts
const employeeId = event['employee_id'] as string;
const offerLetterDocumentId = event['offer_letter_document_id'] as string;
const signingBonusAmount = Number(event['signing_bonus_amount'] ?? 0);

await context.step('send offer letter document', async () => {
  await client.workflowActionExecutions.create({
    action_type: 'send_document',
    payload: {
      document: offerLetterDocumentId,
      recipients: [employeeId],
    },
    workflow_definition_id: event['workflow_definition_id'],
  });
});

if (signingBonusAmount > 0) {
  await context.step('schedule signing bonus payment', async () => {
    await client.workflowActionExecutions.create({
      action_type: 'make_payment',
      payload: {
        apply_cap: false,
        description: `Signing bonus for run ${context.function.run_id}`,
        payment_amount: signingBonusAmount,
        recipients: [employeeId],
      },
      workflow_definition_id: event['workflow_definition_id'],
    });
  });
}
```

### Idempotency

Code inside `context.step()` may be replayed or retried by the durable runtime. Design side-effecting steps to be idempotent when possible.

```ts
await context.step('make relocation payment', async () => {
  await client.workflowActionExecutions.create({
    action_type: 'make_payment',
    payload: {
      apply_cap: true,
      description: `Relocation payment for durable run ${context.function.run_id}`,
      payment_amount: 250000,
      recipients: [employeeId],
    },
    workflow_definition_id: event['workflow_definition_id'],
  });
});
```

## Sleep and scheduling

Use `context.sleep()` for relative delays and `context.sleepUntil()` for absolute wake-up times.

```ts
// Relative delays
await context.sleep('wait 15 minutes', '15 minutes');
await context.sleep('wait 1 day', '1 day');
await context.sleep('wait 5 seconds', 5000); // milliseconds

// Absolute times
await context.sleepUntil('wait until deadline', new Date('2025-12-31T23:59:59Z'));
await context.sleepUntil('wait until epoch timestamp', 1767225599000);
await context.sleepUntil('wait until ISO timestamp', '2025-12-31T23:59:59Z');
```

`DurableDuration` accepts a `number` or `string`. `DurableTimestamp` accepts a `number`, `Date`, or `string`.

## Waiting for external events

Use `context.waitForEvent()` when a function should pause until another system sends an event back to the run. The method returns a structured result with `kind: 'event'` when the event arrives, or `kind: 'timeout'` when the timeout expires.

```ts
type ApprovalEvent = {
  approved: boolean;
  reviewerId: string;
  reason?: string;
};

const approval = await context.waitForEvent<ApprovalEvent>('wait for manager approval', {
  type: 'manager_approval',
  timeout: '3 days',
});

if (approval.isTimeout()) {
  context.fail('Manager approval timed out', {
    code: 'approval_timeout',
    details: approval,
  });
}

if (!approval.payload.approved) {
  context.fail('Manager rejected the request', {
    code: 'approval_rejected',
    details: approval.payload,
  });
}
```

## Failing a durable function

Use `context.fail()` to stop the run with a human-readable message and optional structured failure metadata.

```ts
context.fail('The requested employee could not be found', {
  code: 'employee_not_found',
  details: {
    employeeId,
    runId: context.function.run_id,
  },
});
```

Because `fail()` returns `never`, TypeScript understands that code after it is unreachable.
