> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.ekanos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Triggers

> Test webhooks, schedules, and OAuth handlers locally.

The Triggers surface reads your declared webhooks, schedules, and OAuth straight from your `definition` and lets you run them in fixtures or live mode. Set `definition` on the registry entry and the surface shows a card per webhook and schedule, plus an OAuth declaration card and the Local OAuth panel. Without `definition`, the surface explains what to add.

## Setup

```ts theme={null}
import { asHarnessDefinition } from '@ekanos/harness/registry';
import { integration as myDefinition } from '@you/my-integration';

{
  slug: 'my-integration',
  definition: asHarnessDefinition(myDefinition),

  triggerMocks: {
    storage: { account: { /* … */ }, user: {} },
    secrets: { account: { api_key: 'test_key' } },
    fetchHandlers: [/* … */],
  },

  oauth: {
    setup: { /* … */ },
    connected: { default: true, empty: false },
  },
}
```

`asHarnessDefinition()` erases the storage generic so the definition can sit in the heterogeneous registry array. The mock context is built from the definition's own `storage` schemas and validates every read and write against them at runtime.

## Webhooks

Each declared webhook gets a card, titled with its `id`, with a payload editor seeded from `examplePayload` and a **Deliver webhook** button:

```ts theme={null}
// In your definition
webhooks: [
  {
    id: 'order-created',
    description: 'Fired when a new order is placed',
    payloadSchema: z.object({ orderId: z.string() }),
    signature: 'none',
    examplePayload: { orderId: 'order_123' },
    handler: async (ctx, event) => {
      const { orderId } = event.payload as { orderId: string };
      await ctx.fetch('https://api.acme.com/orders/' + orderId);
      return { status: 'processed' };
    },
  },
],
```

On the Triggers surface:

1. Edit the JSON payload in the editor
2. Click **Deliver webhook**
3. The payload is validated against `payloadSchema`; an invalid one never reaches the handler
4. Your real handler runs against the mock (fixtures) or live context, and the result appears below the card

<Note>
  Signature verification is logged as skipped — it is the transport's job in
  every environment. A webhook that declares `signature.scheme` shows it as a
  badge on its card (algorithm, signed payload, keyed pairs, timestamp source
  and replay window), so you can read back exactly what the host ingress will
  compute. See [Webhooks](/sdk/surfaces/webhooks).
</Note>

## Schedules

Each declared schedule gets a card, titled with its `id` and showing its `cron`, with a **Run now** button:

```ts theme={null}
// In your definition
schedules: [
  {
    id: 'sync-data',
    description: 'Sync data from Acme',
    cron: '0 * * * *',
    handler: async (ctx) => {
      await ctx.fetch('https://api.acme.com/sync');
      await ctx.storage.account.set('lastSync', { at: Date.now() });
      return { status: 'completed' };
    },
  },
],
```

**Run now** invokes the handler with `trigger: 'manual'`; the host scheduler uses `trigger: 'schedule'`, so your handler must be safe under both.

## OAuth

A declared `oauth` gets an **OAuth declaration** card showing:

* Authorization URL and token URL
* Declared scopes
* PKCE (yes/no)
* The client id and client secret secret names
* Egress coverage: one badge per endpoint origin, marked as failing if your `egress` does not cover it

Below that, the **Local OAuth** panel (shown in both modes, so you can set up before switching) walks you through:

1. Creating an app in your vendor's console and adding the redirect URI (with a Copy button)
2. Enabling the declared scopes
3. Pasting the credentials and saving them

<Tip>
  The checklist is derived from your declaration. Add `oauth.setup` to the harness entry for what it cannot know — where the console is and what the provider calls each key:

  ```ts theme={null}
  oauth: {
    setup: {
      consoleUrl: 'https://admindemo.docusign.com/apps-and-keys',
      consoleLabel: 'DocuSign Admin → Apps and Keys',
      notes: ['Integration Key is the client id; add a Secret Key — shown once.'],
    },
  }
  ```
</Tip>

### Fixtures mode

In fixtures mode, the harness answers `oauth/status` from the registry entry's `oauth.connected` field, so an OAuth widget renders with zero traffic and you do not hand-write that fixture. It defaults to connected. Pass a boolean, or a per-variant map:

```ts theme={null}
oauth: {
  connected: {
    empty: false,     // show the Connect prompt under the `empty` variant
  },
}
```

### Live mode

In live mode, the shell runs the real authorize → callback → `onTokens` loop through your developer app credentials. See [Live Mode](/harness/live-mode#oauth-loop-on-localhost).

## triggerMocks

The mock context is built from your definition's schemas and seeded from `triggerMocks`:

```ts theme={null}
triggerMocks: {
  storage: {
    account: {
      config: { merchantId: 'test_456', env: 'sandbox' },
    },
    user: {
      preferences: { theme: 'light' },
    },
  },
  secrets: {
    account: {
      api_key: 'test_key_abc',
      webhook_secret: 'test_secret_xyz',
    },
  },
  fetchHandlers: [
    {
      match: /^https:\/\/api\.acme\.com/,
      respond: () => Response.json({ dynamic: true }),
    },
  ],
},
```

### storage

Storage rows per scope, validated against your definition's `storage` schemas. Keep them fake: everything in `harness.config.ts` is compiled into the browser bundle.

```ts theme={null}
storage: {
  account: {
    config: { merchantId: 'test_456' },
    state: { lastSyncAt: 1234567890 },
  },
  user: {
    prefs: { theme: 'light' },
  },
}
```

### secrets

Secrets by tier for the fixtures-mode mock context. `account` is writable through `ctx.secrets.set()`; `admin` stands in for admin-issued credentials and rejects writes. In live mode, admin secrets come from `.ekanos/local-secrets.json` instead.

```ts theme={null}
secrets: {
  account: { acme_api_key: 'test_key' },
  admin: { acme_client_secret: 'test_secret' },
}
```

The harness logs a console warning when an entry seeds `triggerMocks.secrets`, because those literals are compiled into the browser bundle and the `.next` build output.

### fetchHandlers

Only for responses that must be **dynamic or stateful**. Your `fixtures` already answer `ctx.fetch` (see [Fixtures](/harness/fixtures)), so reach for handlers only when you need something that changes.

A string `match` matches URLs that start with it; a RegExp is tested against the full URL. Handlers are tried before fixtures and win for the URLs they match. A handler call that nothing answers gets the same `NoRecordedResponseError` a widget gets, rather than `createMockContext()`'s default empty `200`.

```ts theme={null}
fetchHandlers: [
  {
    match: 'https://api.acme.com/webhooks',
    respond: () => Response.json({ registered: true }),
  },
  {
    match: /^https:\/\/dynamic\./,
    respond: (request) => {
      if (request.method === 'GET') {
        return Response.json({ count: 42 });
      }
      return new Response(null, { status: 405 });
    },
  },
],
```

<Warning>
  Do NOT re-author your vendor payloads as fetch handlers. That was the
  double-authoring HTTP fixtures exist to kill — the same data in two shapes
  with nothing relating them. Use fixtures for everything that's not dynamic;
  handlers only when responses have to change per-request or carry state.
</Warning>

## onActivate

The `onActivate` hook runs on the **Activation surface**, not the Triggers surface. Submitting the activation form there runs your real hook (if declared) against the context the toolbar mode selects—browser mock in fixtures, live context in live—and shows the outcome (ran, skipped because not declared, or threw with the message) below the connect dialog.

## Invocation results

When a webhook or schedule runs, the card shows a result panel: a summary badge (with detail, if any), the handler's logs, and the account and user storage after the run.

In fixtures mode every invocation runs in the browser against one mock context per visit, seeded from `triggerMocks`, so state accumulates across invocations; leaving the page resets it.

In live mode, the buttons post to `/api/integrations/<slug>/trigger` and handlers run on the server-held live context, which persists until the dev server restarts.

## Troubleshooting

### Webhook validation fails

Check your payload against the `payloadSchema`. An invalid payload never reaches the handler.

### Handler error: no recorded response

A handler's `ctx.fetch` call didn't match a fixture. See [Fixtures](/harness/fixtures) for how to record it. The error message shows the URL to paste (with credentials redacted).

### Payload editor JSON is invalid

Check for trailing commas, missing quotes, or unescaped special characters.

### Triggers surface is empty

Your entry has no `definition` field. Add it with `asHarnessDefinition(myDefinition)`.

### Egress coverage badge is red

Your `oauth.provider.authorizationUrl` or `tokenUrl` origin is not in your definition's `egress`. Add it or check for typos.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.