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

# Common errors and how to fix them

> Real errors you might hit and their solutions

# Common errors and how to fix them

Messages below are quoted from the packages. Values in `<angle brackets>` (and example slugs, keys, and hosts) vary with your project.

## Definition errors (at import time)

`defineIntegration()` throws one error listing every problem, each prefixed with the path that failed:

```
Invalid integration definition for "<slug>":
  - <path>: <message>
```

`ekanos validate` reports the same problems as `definition.schema` or `definition.load` findings.

### Slug is not kebab-case

```
slug: slug must be kebab-case ([a-z0-9] segments separated by single hyphens), e.g. "acme-crm" — it becomes the product slug, route segment, and MCP namespace.
```

**Fix:** Use lowercase `[a-z0-9]` segments separated by single hyphens:

```ts theme={null}
slug: 'my-integration'; // ✓
slug: '123-integration'; // ✓ (digits are allowed)
slug: 'My_Integration'; // ✗ (uppercase, underscore)
slug: 'my--integration'; // ✗ (double hyphen)
```

***

### Unrecognized key

```
(root): Unrecognized key(s) in object: 'productId', 'kind'
```

When an unrecognized key is among the failures, the error ends with this reminder:

```
Host-assigned fields are never partner-authorable: productId, kind, trust tier, and machine exposure (credentialModel) do not exist on IntegrationDefinition, per-tool effect/sensitivity belong in the `proposes` block, and host-resolved widget fields (productId, widgetConfigId, workspaceId, collapsed, isPinned, health, integrationMetadata) are populated by the platform at runtime — remove them from the definition.
```

**Cause:** The schema is strict everywhere. Either you declared a host-assigned field, or you have a typo in a key name.

**Fix:** Remove the host-assigned fields listed above, and move per-tool `effect`/`sensitivity` into `proposes.tools`. If you are confident the key is valid, your project's `@ekanos/integration-schema` may be newer than your CLI — run `ekanos upgrade --check`.

***

### Widget id is invalid or duplicated

```
components.widgets.0.id: Widget ids are kebab-case and globally unique, e.g. "acme-crm-pipeline" — prefix with the integration slug to stay collision-free.
```

```
components.widgets: Widget id "<id>" is declared more than once — give every widget a unique id.
```

**Fix:** Use kebab-case, prefixed with your slug:

```ts theme={null}
components: {
  widgets: [
    {
      id: 'my-integration-summary',  // ✓
      // …
    },
  ],
}
```

***

### Tool name is not snake\_case

```
tools.0.name: Tool names are lowercase snake_case starting with a letter, e.g. "list_invoices" — the model calls them by this exact string.
```

**Fix:**

```ts theme={null}
name: 'get_payments'; // ✓
name: 'getPayments'; // ✗ (camelCase)
name: 'get-payments'; // ✗ (hyphens)
```

***

### Webhook, schedule, or action name is not kebab-case

```
webhooks.0.id: Webhook ids are kebab-case, e.g. "payment-updated" — the host ingress route addresses the handler by this exact string.
```

```
schedules.0.id: Schedule ids are kebab-case, e.g. "daily-reconcile" — the host scheduler addresses the handler by this exact string.
```

Action names (the keys of `actions`) follow the same rule:

```
Action names are kebab-case, e.g. "create-record" — the host route and useIntegrationAction() address the handler by this exact string.
```

**Fix:**

```ts theme={null}
id: 'payment-updated'; // ✓
id: 'payment_updated'; // ✗
```

Vendor event names like `payment_intent.succeeded` are never valid ids — map them to a kebab-case id and put the vendor's name in `description`.

***

### Cron expression is invalid

```
schedules["<id>"]: Cron expression "0 0 0 * *" is invalid: 0 is out of range for the day-of-month field (1-31). Expected 5 space-separated fields (minute hour day-of-month month day-of-week), numeric values only, with "*", lists ("1,15"), ranges ("1-5"), and steps ("*/15", "0-30/5") — e.g. "0 6 * * *" for 06:00 daily.
```

**Cause:** Cron has strict numeric bounds: minute 0-59, hour 0-23, day-of-month 1-31, month 1-12, day-of-week 0-7. Names (`MON`), macros (`@daily`), and a seconds field are not supported.

**Fix:**

```ts theme={null}
cron: '0 6 * * *'; // ✓ 06:00 daily
cron: '0 0 1 * *'; // ✓ Monthly on the 1st
cron: '*/15 * * * *'; // ✓ Every 15 minutes
cron: '0 0 0 * *'; // ✗ day-of-month 0 is out of range
```

See [Schedules](/sdk/surfaces/schedules) for the full cron rules.

***

### OAuth endpoint is not covered by egress

```
oauth.provider.authorizationUrl: its origin https://provider.example is not covered by this definition's egress list []. The OAuth code exchange runs through the integration's declared egress, so add "https://provider.example" (or a covering "*." wildcard entry) to `egress` — widening egress is a security-posture change reviewed at promotion.
```

The same message appears for `oauth.provider.tokenUrl`.

**Fix:** Add the origin of both endpoints:

```ts theme={null}
egress: [
  'https://provider.example', // covers /oauth/authorize and /oauth/token
];
```

***

### Storage key is invalid

A key is `"<dataType>"` or `"<dataType>/<subtype>"`. Two different messages can fire:

```
storage.account.a/b/c: Storage key "a/b/c" is malformed. Keys are "<dataType>" or "<dataType>/<subtype>" in lowercase [a-z0-9_-], with at most one "/" and no empty segment.
```

```
storage.account.my-config/location: Storage key "my-config/location" is invalid for the account scope: "my-config" is not an allowed account data_type. Use one of [activation, settings, metrics_summary, sync_state, cache] (the database CHECK constraint).
```

**Cause:** The part before the `/` must be an allowed data type for its scope: `activation`, `settings`, `metrics_summary`, `sync_state`, or `cache` under `storage.account`; `config`, `preferences`, or `cache` under `storage.user`. `secret` is reserved — use `ctx.secrets` instead.

**Fix:**

```ts theme={null}
'settings/location'; // ✓ (account)
'cache/forecast'; // ✓ (account or user)
'preferences/units'; // ✓ (user)
'my-config/location'; // ✗ (my-config is not a data type)
'settings/location.v2'; // ✗ (dots are not allowed)
'a/b/c'; // ✗ (two slashes)
```

***

## Validation findings (`ekanos validate`)

`ekanos validate` exits `3` (`validation`) when there is at least one error-severity finding. Warnings alone do not fail it.

### project.host-compiler-rules (warning)

```
tsconfig.json does not set noUncheckedIndexedAccess in compilerOptions. The gate re-checks your submission under the host's own stricter compiler rules — a project that typechecks cleanly under its own laxer tsconfig can still fail that pass.
```

**Cause:** The host gate's `checks/typecheck-host-rules` pass compiles under `strict` and `noUncheckedIndexedAccess`. `validate` warns; `ekanos publish` escalates this finding to an error and refuses to submit.

**Fix:** Add to `tsconfig.json`:

```json theme={null}
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}
```

***

### project.slug-agreement (error)

```
ekanos.json declares slug "my-integration" for this entry, but the definition says "my-payments". The slug addresses the integration everywhere — its harness route, its product record, its widget ids — so the two must agree.
```

**Fix:** Change one to match the other:

```json theme={null}
// ekanos.json
{ "slug": "my-integration", "entry": "src/integration.ts" }
```

```ts theme={null}
// src/integration.ts
export const integration = defineIntegration({
  slug: 'my-integration', // must match ekanos.json
  // …
});
```

***

### collision.widget-id (error)

```
widget id "shared-summary" is declared by [my-integration, other-integration] — widget ids are global (widget_config rows key on them); prefix yours with the integration slug.
```

**Cause:** Two integrations in the same `ekanos.json` declare the same widget id. Tool names collide the same way (`collision.tool-name`), compared by their slug-prefixed effective names.

**Fix:** Prefix with your integration slug:

```ts theme={null}
id: 'my-integration-summary'; // ✓
```

***

### icons/pro-only (warning)

```
"fa-<name>" is not in the Font Awesome Free set. Ekanos itself loads Font Awesome Pro, so this renders fine there, but the generated harness (and any other Free-only host) shows the circle-question placeholder instead of the intended glyph.
```

**Cause:** The generated harness loads Font Awesome Free only.

**Fix:** Pick a Free icon name. When a Free equivalent is known, the finding's hint names it (`Use "fa-<alternative>" instead — the nearest Free equivalent.`).

***

## Runtime errors

### Cannot find module @ekanos/sdk

**Cause:** Dependencies are not installed.

**Fix:** Run `pnpm install` (or your package manager's equivalent) after scaffolding.

***

### StorageValidationError

`ctx.storage` throws `StorageValidationError` (`code: 'STORAGE_VALIDATION_FAILED'`, with `scope`, `key`, and `hint`) when you access an undeclared key, write a value that fails the key's schema, or read stored data that no longer matches the schema.

**Fix for schema changes:** Schema evolution is manual. Never loosen a schema in place — add a new key or widen with a union:

```ts theme={null}
// Add a new key alongside the old one
'settings/location': LocationSchema,
'settings/location-v2': NewLocationSchema,

// Or accept both shapes
'settings/location': z.union([LocationSchema, NewLocationSchema]),
```

When adding a field to an existing schema, make it `.optional()` — a required field throws on every row written before the change.

***

### EgressDeniedError

```
Egress to https://api.untrusted.example is not in this integration's allowlist. Declare the origin in the integration's egress list — an exact origin ("https://api.example.com") or a "*." subdomain wildcard ("https://*.example.com") — or call an allowed origin instead. Allowlist changes are a security-posture change reviewed at promotion.
```

**Cause:** A handler called `ctx.fetch` for an origin not covered by `egress` (redirect targets included). The error has `code: 'EGRESS_DENIED'` and an `origin` property. On the host, an allowlisted name that resolves to a private, loopback, or link-local address is also refused, with a `reason`.

**Fix:** Add the origin to `egress`:

```ts theme={null}
egress: [
  'https://api.acme.example',
  'https://api.untrusted.example', // add this
];
```

Then validate and republish.

***

### SecretAccessError

```
Secret "platform_key" resolves only from an admin-issued tier (source or global) and cannot be written from integration code. secrets.set() rotates account-tier values only — the ones this integration stored for the current account. If this credential needs a new value, an admin must update it where it was issued.
```

**Cause:** `ctx.secrets.set()` targeted a name that exists only in an admin-issued (source or global) tier. The error has `code: 'SECRET_ACCESS_DENIED'`.

**Fix:** Only write account-tier secrets — for example, OAuth tokens stored from `onTokens`. In tests, `createMockContext({ secrets: { account: { … }, admin: { … } } })` reproduces the split.

***

### NoRecordedResponseError (harness fixtures mode)

```
No recorded response for GET https://api.acme.example/payments.
```

The message continues with the exact fixture line to paste. The console also logs a line such as `[harness:<slug>] refused GET https://api.acme.example/payments`.

**Cause:** The harness is in fixtures mode and the request has no matching fixture for the active variant. It was refused, not sent.

**Fix:** Record the fixture in `harness.config.ts`:

```ts theme={null}
fixtures: {
  default: [
    {
      request: 'GET https://api.acme.example/payments',
      response: { payments: [] },
    },
  ],
}
```

Or add a `live` block to the entry and switch the toolbar to Live API.

***

### No IntegrationActivationProvider is mounted

```
No IntegrationActivationProvider is mounted. The host application must bind its activation server actions (see @ekanos/sdk/hooks IntegrationActivationProvider) above any component that activates integrations.
```

**Cause:** `useActivateIntegration()` / `useDeactivateIntegration()` reject with this message at call time when no provider is mounted.

**Fix:** The host and the dev harness both mount the provider. If you render an activation form in isolation (a test or Storybook), mount it yourself:

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

<IntegrationActivationProvider actions={{ activate, deactivate }}>
  <YourActivationForm />
</IntegrationActivationProvider>;
```

***

### ctx.storage.user throws for machine actors

The SDK's mock context throws:

```
ctx.storage.user is unavailable for machine actors: there is no user to scope the row to. Store machine-written state in ctx.storage.account instead.
```

**Cause:** `ctx.storage.user` needs a user. A machine actor (`ctx.actor.kind === 'machine'`) has none.

**Fix:** Use account-scoped storage for anything a machine actor — such as a workflow — can reach:

```ts theme={null}
// ✗ Throws for machine actors
await ctx.storage.user.get('preferences/units');

// ✓ Works for any actor
await ctx.storage.account.get('settings/location');
```

***

## CLI errors (`ekanos publish`, `ekanos status`, …)

In JSON mode, errors arrive as `{ ok: false, error: { code, message, hint } }`. The exit code and `code` always pair up.

### Exit 4: `auth_required`

```
Not logged in to https://your-ekanos-host.
```

Hint: `Run "ekanos login --host https://your-ekanos-host".`

Other `auth_required` messages cover an expired or revoked session (for example, `The stored session for https://your-ekanos-host is no longer valid.`) and a pending MFA requirement.

**Fix:**

```bash theme={null}
ekanos login --host https://your-ekanos-host
```

***

### Exit 5: `forbidden`

```
This account does not hold a seat on source "other-source" on https://your-ekanos-host.
```

The hint lists your actual seats, for example: `You hold a seat on: my-source (developer). Run "ekanos use <slug>" with one of these.`

**Cause:** `ekanos use` or `ekanos publish` targeted a source you hold no seat on.

**Fix:**

```bash theme={null}
ekanos sources          # See which sources you hold a seat on
ekanos use my-source
ekanos publish
```

***

### Exit 9: `precondition_failed` (schema skew)

```
The project's @ekanos/integration-schema@0.4.1 is newer than 0.4.0, the version this @ekanos/cli build validates with. Findings from "ekanos validate" are UNRELIABLE in this state: a field the newer schema added parses here as an unrecognized key, and the usual hint for that tells you to delete code that is actually correct.
```

**Cause:** Your project resolves a newer `@ekanos/integration-schema` than the CLI bundles. `validate` and `publish` fail before loading your definition.

**Fix:**

```bash theme={null}
ekanos upgrade
```

Or pin the CLI by hand with `npm i -D @ekanos/cli@latest`, then re-run.

***

### Exit 10: `gate_failed`

`ekanos publish` refuses before submitting when the local findings pass has errors:

```
Refusing to publish: 2 validation findings.
```

`ekanos status --watch` exits 10 when the host gate rejects the submitted version:

```
my-integration@1.0.0 was rejected by the gate (2 errors).
```

**Fix:** Read the findings (in `data.findings`, or with `ekanos status --findings`), fix them, bump the version, and publish again. Common gate failures:

* TypeScript errors under the host's stricter rules (`checks/typecheck-host-rules`)
* Test failures (`checks/tests`)

***

## Still stuck?

1. **Read the hint** — every CLI error carries an imperative `hint`
2. **Run with `--json`** — `ekanos validate --json` gives the full findings list
3. **Test locally** — `ekanos test` before publishing

Email **[npm@govastly.com](mailto:npm@govastly.com)** with:

* The exact error message
* Your `ekanos.json` and the relevant section of `src/integration.ts`
* The output of `ekanos validate --json`


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