> ## 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.

# Surfaces at a glance

> Quick reference for widgets, tools, webhooks, schedules, and actions

# Surfaces at a glance

Every integration declares surfaces — the things it renders or runs. Here's what each one does and when to use it.

## Widgets

Client-side components that render in the dashboard grid.

**When to use:** Primary interface for viewing data, displaying metrics, or interactive controls.

```ts theme={null}
components: {
  widgets: [
    {
      id: 'acme-summary',
      name: 'Payment Summary',
      component: SummaryWidget,
      widgetState: 'active',
      isCollapsible: true,
      isPinnable: false,
      aiFooterEnabled: true,
      layouts: {
        lg: { x: 0, y: 0, w: 4, h: 8 },
        md: { x: 0, y: 0, w: 4, h: 8 },
        sm: { x: 0, y: 0, w: 2, h: 8 },
      },
    },
  ],
}
```

**Key points:**

* Render through the `Widget.*` compound API from `@ekanos/sdk/components`
* Read `clientReadable` account storage with `fetchIntegrationStorage()` (`@ekanos/sdk/hooks`)
* Call APIs from the browser with `useIntegrationFetch()` (`@ekanos/sdk/hooks`)
* Write through a declared action with `useIntegrationAction()`
* `isPinnable` adds "Pin to board" / "Unpin from board" to the `Widget.Header` ⋯ menu
* Preview on the harness's Widgets surface

## Tools

Functions the Ekanos assistant can call. The assistant reads the description and parameters, decides when to invoke, and shows results to the user.

**When to use:** Anything the assistant should be able to look up or do on the user's behalf.

```ts theme={null}
tools: [
  {
    name: 'list_recent_payments',
    description: 'List the 10 most recent payments.',
    parameters: {
      type: 'object',
      properties: {},
      required: [],
      additionalProperties: false,
    },
    outputExample: { payments: [], total: 0 },
    async run(ctx, args) {
      const response = await ctx.fetch('https://api.acme.example/payments');
      if (!response.ok) return { error: 'Failed to fetch' };
      return response.json();
    },
  },
];
```

**Key points:**

* Invoked by the assistant (and, where the host allows it, the workflow engine) — not deterministic
* Return `{ error: '…' }` for expected failures the model should explain
* Throw for genuine faults
* `parameters` is a narrow JSON Schema slice: the top level accepts only `type: 'object'`, `properties`, `required`, and `additionalProperties`
* The assistant sees a slug-namespaced name: `acme-payments` + `list_recent_payments` → `acme_payments_list_recent_payments`

## Webhooks

Inbound events from your provider. Ekanos receives them, validates the signature and payload, and calls your handler.

**When to use:** Responding to real-time events (payment status change, customer created, etc.).

```ts theme={null}
webhooks: [
  {
    id: 'payment-updated',
    description: 'Payment status change from Acme.',
    payloadSchema: PaymentUpdatedSchema,
    signature: {
      header: 'x-acme-signature',
      secretName: 'acme_webhook_secret',
    },
    examplePayload: { payment: { id: '123', status: 'succeeded' } },
    async handler(ctx, event) {
      const { payment } = event.payload;
      await ctx.storage.account.set('cache/last-payment', payment);
      return { status: 'processed', detail: `Updated to ${payment.status}` };
    },
  },
];
```

**Key points:**

* The handler receives a payload already validated against `payloadSchema`; a failing payload never reaches it
* Return `{ status: 'processed' }` when handled (2xx)
* Return `{ status: 'ignored' }` for valid but irrelevant events (2xx — the sender must not retry)
* Throw when you could not handle a valid event (retryable failure)
* Signature verification is the transport's job — don't verify it yourself. Use `signature: 'none'` for an unsigned source

## Schedules

Cron-driven jobs that run at regular intervals. Same `ctx` as every other handler.

**When to use:** Regular syncing (hourly reconcile, daily summary) or maintenance tasks.

```ts theme={null}
schedules: [
  {
    id: 'hourly-sync',
    description: 'Sync payments every hour.',
    cron: '0 * * * *', // minute hour day-of-month month day-of-week
    async handler(ctx, invocation) {
      const apiKey = await ctx.secrets.get('acme_api_key');
      if (!apiKey) return { status: 'skipped', detail: 'No API key' };

      const response = await ctx.fetch('https://api.acme.example/payments');
      if (!response.ok) throw new Error(`Acme returned ${response.status}`);

      await ctx.storage.account.set('cache/payments', await response.json());
      return { status: 'completed', detail: 'Synced 42 payments' };
    },
  },
];
```

**Key points:**

* 5-field cron syntax (minute hour day-of-month month day-of-week)
* Numeric only, with `*`, lists `1,15`, ranges `1-5`, and steps `*/15`, `0-30/5` — no names, no `@daily` macros, no seconds field
* Return `{ status: 'completed' }` when the run did its work
* Return `{ status: 'skipped' }` when it correctly did nothing
* Throw for failures — the transport records a failed run
* `invocation.trigger` is `'schedule'` or `'manual'` (a human pressed "Run now")

## Actions

Widget-invoked handlers for deterministic writes. A user clicks Save in a widget, the action runs, the widget handles the result.

**When to use:** Deterministic writes (save settings, create a record, delete something).

```ts theme={null}
actions: {
  'save-settings': {
    description: 'Save user preferences.',
    input: SaveSettingsSchema,
    output: SettingsResultSchema,  // optional — validates response
    async handler(ctx, input) {
      const response = await ctx.fetch('https://api.acme.example/settings', {
        method: 'POST',
        body: JSON.stringify(input),
      });
      if (!response.ok) throw new Error('Save failed');
      return response.json();
    },
  },
}
```

**From the widget:**

```tsx theme={null}
import { useIntegrationAction } from '@ekanos/sdk/hooks';

const saveSettings = useIntegrationAction<SaveSettingsInput, SettingsResult>(
  'acme-payments',
  'save-settings',
  { accountId },
);

<Button
  onClick={() => saveSettings.mutate(formValues)}
  disabled={saveSettings.isPending}
>
  Save
</Button>;
```

A rejected mutation's `error` is always an `IntegrationActionError` with `status`, `code`, and — for a 400 `invalid_input` — your schema's zod `issues`.

**Key points:**

* Invoked deterministically (a user clicks Save, not LLM intent)
* `input` is a required zod schema; `output` is an optional zod schema — without it, the resolved value passes through unvalidated
* A thrown error becomes a generic `handler_error` response; throw `PublicActionError` (`@ekanos/sdk/integration`) to show your own message verbatim
* Response codes: `action_not_found` (404), `invalid_input` (400), `rate_limited` (429), `handler_error` (500)
* No LLM governance (no `parameters`, no `proposes` classification)

## OAuth

Secure connection to a third-party provider. The transport runs the authorize flow and code exchange, then calls your `onTokens` hook — you persist the tokens.

**When to use:** Connecting to a provider that requires OAuth (versus API key).

```ts theme={null}
oauth: {
  provider: {
    authorizationUrl: 'https://provider.example/oauth/authorize',
    tokenUrl: 'https://provider.example/oauth/token',
    scopes: ['payments:read', 'refunds:write'],
    pkce: true,
  },
  credentials: {
    clientIdSecretName: 'provider_client_id',
    clientSecretSecretName: 'provider_client_secret',
  },
  async onTokens(ctx, tokens) {
    await ctx.secrets.set('provider_access_token', tokens.accessToken);
    if (tokens.refreshToken) {
      await ctx.secrets.set('provider_refresh_token', tokens.refreshToken);
    }
  },
}
```

**Key points:**

* Both endpoint origins must be covered by your `egress` allowlist — `defineIntegration()` rejects the declaration otherwise
* Tokens are passed to `onTokens`; store them with `ctx.secrets.set()`
* The redirect URI is host-owned and built from the host's own origin — don't declare one (`provider.redirectUri` is an unrecognized key)
* There is no host-driven token refresh yet
* In the harness, live mode runs the real OAuth loop on localhost

## onActivate

Optional hook, `(ctx) => Promise<void>`, for cache seeding and eager credential checks at connect time.

**When to use:** Eager data loading or testing credentials at connect time.

```ts theme={null}
onActivate: async (ctx) => {
  const apiKey = await ctx.secrets.get('acme_api_key');
  if (!apiKey) return; // Not an error — nothing to seed yet

  const response = await ctx.fetch('https://api.acme.example/account');
  if (!response.ok) throw new Error('Invalid API key');

  // Seed the cache
  await ctx.storage.account.set('cache/account', await response.json());
};
```

**Key points:**

* Errors are NON-FATAL (logged, shown as a warning, connection stays active) — it cannot reject a connection
* Runs after an activation is first persisted AND after `activationData` updates
* Runs in the harness's Activation surface and under `invokeActivate()`; the production host does not call it yet

## Summary table

| Surface | Invocation | Handler | Response | Use for |
| - | - | - | - | - |
| Widget | User views | Client component | UI render | Display data, interactive controls |
| Tool | LLM intent | `run(ctx, args)` | JSON or `{ error }` | Assistant reads/writes |
| Webhook | Vendor event | `handler(ctx, event)` | `{ status }` | Real-time event reactions |
| Schedule | Cron timer | `handler(ctx, invocation)` | `{ status }` | Regular syncing, maintenance |
| Action | Widget Save button | `handler(ctx, input)` | JSON (optionally `output`-validated) | Deterministic writes |
| OAuth | User clicks Connect | `onTokens(ctx, tokens)` | None (store tokens) | Secure token exchange |
| onActivate | After connect | `handler(ctx)` | None (side effects) | Cache seeding, validation |


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