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

# How integrations are structured

> The anatomy of an Ekanos integration and how it's organized

# How integrations are structured

An integration is a package that exports a validated definition. Everything you declare upfront shapes how Ekanos treats your integration.

## One call, complete declaration

Every integration starts with `defineIntegration()`:

```ts theme={null}
import { defineIntegration } from '@ekanos/sdk/integration';

export const integration = defineIntegration<MyStorage>({
  // Required: identity
  slug: 'acme-payments',
  name: 'Acme Payments',
  description: 'View payment volume and issue refunds.',
  version: '1.0.0',

  // Optional: what it does
  components: {
    widgets: [/* … */],
    marketplaceTile: AcmeMarketplaceTile,
    activationForm: AcmeActivationForm,
  },

  tools: [/* … */],
  webhooks: [/* … */],
  schedules: [/* … */],
  actions: {/* … */},
  oauth: {/* … */},

  // Optional: what it can touch
  storage: { account: {}, user: {} },
  egress: ['https://api.acme.example'],

  // Optional: what the marketplace shows users
  capabilities: [/* … */],
  permissions: [/* … */],
});
```

One object, validated at import time. If something is wrong, `defineIntegration()` throws a remediation message immediately.

## The main surfaces

### Widgets

Dashboard cards that render in the grid. They read `clientReadable` storage with `fetchIntegrationStorage()`, call APIs from the browser with `useIntegrationFetch()`, and write through actions with `useIntegrationAction()`. (`ctx.fetch` is server-side only.)

```ts theme={null}
components: {
  widgets: [
    {
      id: 'acme-payments-summary',
      name: 'Payments Summary',
      component: PaymentsSummaryWidget,
      widgetState: 'active',
      layouts: { lg: { x: 0, y: 0, w: 4, h: 8 } },
    },
  ],
}
```

### Tools

Functions the Ekanos assistant can call. They receive user intent, call your vendor API, and return structured results. The assistant reads the `description` and `parameters` to understand what they do.

```ts theme={null}
tools: [
  {
    name: 'get_recent_payments',
    description: 'Fetch recent payments for the account.',
    parameters: {
      type: 'object',
      properties: { limit: { type: 'number' } },
      required: [],
      additionalProperties: false,
    },
    async run(ctx, args) {
      const response = await ctx.fetch('https://api.acme.example/payments');
      return response.json();
    },
  },
];
```

### Webhooks

Inbound events from your provider. You declare the expected payload schema and a handler. The host verifies the signature and calls your handler with the validated payload.

```ts theme={null}
webhooks: [
  {
    id: 'payment-updated',
    description: 'Acme posts one event per payment status change.',
    payloadSchema: PaymentUpdatedSchema,
    signature: {
      header: 'x-acme-signature',
      secretName: 'acme_webhook_secret',
    },
    examplePayload: {/* … */},
    async handler(ctx, event) {
      // event.payload is already validated
      return { status: 'processed', detail: '…' };
    },
  },
];
```

### Schedules

Cron-driven jobs. Same `ctx` as every other handler; the handler receives a `ScheduleInvocation` and returns `{ status: 'completed' | 'skipped' }`.

```ts theme={null}
schedules: [
  {
    id: 'hourly-reconcile',
    description: 'Sync the latest payments hourly.',
    cron: '0 * * * *', // 5-field cron: minute hour day month weekday
    async handler(ctx, invocation) {
      const payments = await ctx.fetch('https://api.acme.example/payments');
      await ctx.storage.account.set('cache/payments', await payments.json());
      return { status: 'completed', detail: '…' };
    },
  },
];
```

### Actions

Deterministic write handlers invoked by a widget's Save button. Unlike tools, they have no LLM governance — a human clicks, the handler runs, the result comes back.

```ts theme={null}
actions: {
  'create-refund': {
    description: 'Issue a refund for a payment.',
    input: CreateRefundSchema,
    output: RefundResultSchema,  // optional — validates response
    async handler(ctx, input) {
      const result = await ctx.fetch('https://api.acme.example/refunds', {
        method: 'POST',
        body: JSON.stringify(input),
      });
      return result.json();
    },
  },
}
```

## The context (`ctx`)

Every server-side handler (tools, webhooks, schedules, actions, `oauth.onTokens`, `onActivate`) receives the same capability context:

```ts theme={null}
ctx.storage.account; // Schema-validated per-account state
ctx.storage.user; // Per-user state (throws for machine actors)
ctx.secrets; // get(name), names(), set(name, value)
ctx.fetch(url, init); // Outbound requests (enforces the egress allowlist)
ctx.logger; // debug/info/warn/error(context, message) — pino-style
ctx.actor; // { kind: 'user', userId } | { kind: 'machine', tokenId, createdBy }
ctx.accountId; // The account id (already authorized)
ctx.accountSlug; // string | null
ctx.userId; // null for machine actors
ctx.sourceId; // string | null
ctx.integration; // { slug, productId }
ctx.timezone; // string | null
```

There is no raw database client, no Supabase, no request object. Everything is scoped before your code runs.

## Storage schema

Declare what state you'll hold:

```ts theme={null}
const myStorage = {
  account: {
    'cache/payments': z.object({/* … */}),
    'settings/location': {
      schema: LocationSchema,
      clientReadable: true, // expose to widgets
    },
  },
  user: {
    'preferences/units': z.enum(['celsius', 'fahrenheit']),
  },
} satisfies StorageSchemas;

export const integration = defineIntegration<typeof myStorage>({/* … */});
```

### Reading and writing

```ts theme={null}
// Read
const saved = await ctx.storage.account.get('settings/location');
// → { data, externalId, expiresAt, updatedAt } | null

// Write
await ctx.storage.account.set('cache/payments', body, {
  expiresAt: new Date(Date.now() + 600_000).toISOString(),
});

// Delete
await ctx.storage.account.delete('cache/payments');
```

`get` returns `null` for a missing key and for an entry past its `expiresAt`. A read whose stored data no longer matches the schema throws `StorageValidationError` rather than giving you a shape you didn't expect.

`import type { StorageSchemas } from '@ekanos/sdk'`. A key's part before the `/` must be an allowed data type for its scope: `activation`, `settings`, `metrics_summary`, `sync_state`, or `cache` for `account`; `config`, `preferences`, or `cache` for `user`.

## Egress allowlist

Declare the origins your handlers call:

```ts theme={null}
egress: [
  'https://api.acme.example', // exact origin
  'https://*.acme-cdn.example', // single leading-label wildcard
  'https://api.acme.example:8443', // explicit port
];
```

`ctx.fetch` refuses anything not covered with `EgressDeniedError`, **before opening a socket** — redirect targets included. Entries are https origins only: no path, query, fragment, credentials, or `http:`. Widening this list is a security-posture change reviewed at promotion.

## Naming rules

Everything validates upfront:

| Field | Rule | Example |
| - | - | - |
| `slug` | kebab-case | `acme-payments` |
| `version` | semver, prerelease/build suffix allowed | `1.0.0` |
| Widget ids | kebab-case, **globally unique** | `acme-payments-summary` |
| Tool names | lowercase snake\_case starting with a letter | `get_recent_payments` |
| Webhook ids | kebab-case | `payment-updated` |
| Schedule ids | kebab-case | `hourly-reconcile` |
| Action names | kebab-case | `create-refund` |
| Storage keys | `<dataType>` or `<dataType>/<subtype>`, lowercase `[a-z0-9_-]`, at most one `/`; `dataType` from the scope's allowed list | `settings/location` |
| Cron syntax | 5 fields, numeric | `0 6 * * *` |

Invalid names fail validation with a message that states the rule.

## Where everything lives

A typical project after `ekanos init` and a first `ekanos dev` (the `components/` layout is up to you):

```
my-integration/
  src/
    integration.ts         # the definition
    integration.test.ts
    components/            # your widget, tile, and activation form components
  harness.config.ts        # dev harness registry (written by the first `ekanos dev`)
  ekanos.json              # { slug, entry }
  package.json
  tsconfig.json
  vitest.config.ts
```

`ekanos.json`'s `entry` points at the module to validate. The CLI takes its default export if it has a string `slug`, otherwise the first named export that does. The scaffold exports it as `integration`.

## The full lifecycle

1. **Author** — Write `src/integration.ts` with `defineIntegration()`
2. **Validate** — `ekanos validate` checks syntax, naming, and structure
3. **Test** — `ekanos test` runs your project's test script
4. **Dev** — `ekanos dev` runs the harness and renders everything locally
5. **Publish** — `ekanos publish` validates, packs, and submits
6. **Gate** — The host's submission gate checks the archive (for example `checks/typecheck` and `checks/tests`); follow it with `ekanos status --watch`
7. **Connect** — A user connects through your activation form or OAuth flow
8. **Run** — Handlers execute on schedule, webhook delivery, assistant tool call, or widget action

`defineIntegration()`, `ekanos validate`, and the host all validate against the same `@ekanos/integration-schema` schema. There is no second contract.


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