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

# Naming and validation

> Naming rules for all fields with exact regex patterns and examples.

Every field has specific naming rules enforced at `defineIntegration()` import time. This is the complete reference.

| Field | Rule | Regex | Example |
| - | - | - | - |
| `slug` | kebab-case, globally unique | `^[a-z0-9]+(-[a-z0-9]+)*$` | `acme-payments` |
| `version` | semver, optional prerelease/build | `^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$` | `1.0.0`, `2.1.0-beta.3` |
| `components.widgets[].id` | kebab-case, **globally unique** — prefix with slug | `^[a-z0-9]+(-[a-z0-9]+)*$` | `acme-payments-summary` |
| `tools[].name` | lowercase snake\_case | `^[a-z][a-z0-9_]*$` | `list_recent_payments` |
| `webhooks[].id` | kebab-case | `^[a-z0-9]+(-[a-z0-9]+)*$` | `payment-updated` |
| `schedules[].id` | kebab-case | `^[a-z0-9]+(-[a-z0-9]+)*$` | `daily-reconcile` |
| `schedules[].cron` | 5-field numeric cron | Standard grammar | `0 6 * * *` |
| `actions` keys | kebab-case | `^[a-z0-9]+(-[a-z0-9]+)*$` | `create-record` |
| storage keys | `<dataType>` or `<dataType>/<subtype>`; dataType from the scope's allowed list | `^[a-z0-9_-]+(/[a-z0-9_-]+)?$` | `settings/location`, `cache/forecast` |
| `egress[]` | https origin, optional leading `*.`, optional port | — | `https://api.example.com`, `https://*.cdn.example.com:8443` |
| OAuth endpoints | absolute https, no embedded credentials, origin in egress | — | `https://oauth.provider.com/authorize` |

## Slug

The kebab-case identifier for your integration. Used as:

* Route segment (`/api/integrations/<slug>`)
* MCP namespace prefix
* Widget ID prefix
* Project identifier

Must be globally unique — `validateIntegrationDefinitions()` rejects a duplicate slug, and a slug a first-party integration uses.

```ts theme={null}
// ✅ Valid
slug: 'acme-payments';
slug: 'stripe-connector';
slug: 'crm';

// ❌ Invalid
slug: 'acme_payments'; // underscore
slug: 'AcmePayments'; // capitals
slug: 'acme payments'; // space
slug: 'acme-'; // trailing hyphen
```

## Widget IDs

Globally unique within the entire Ekanos instance. Prefix with your slug to avoid collisions:

```ts theme={null}
widgets: [
  { id: 'acme-payments-summary' }, // ✅ Prefixed
  { id: 'acme-payments-detail' }, // ✅ Prefixed
  { id: 'summary' }, // ❌ Not prefixed — collision risk
];
```

## Tool names

Lowercase snake\_case. The effective name the assistant sees is namespaced by slug:

```ts theme={null}
// Your slug: 'acme-payments'
tools: [
  { name: 'list_payments' },
  // Effective name: 'acme_payments_list_payments' (hyphens → underscores)

  { name: 'acme_payments_refund' },
  // Effective name: 'acme_payments_refund' (no double prefix)
];

// ✅ Valid names
('get_customer');
('list_payments');
('refund_payment');

// ❌ Invalid names
('ListPayments'); // capitals
('list-payments'); // hyphen
('list payments'); // space
```

## Webhook and schedule IDs

Kebab-case. These are your names, not the vendor's event names:

```ts theme={null}
webhooks: [
  { id: 'payment-updated' }, // ✅ Your name
  { id: 'payment_updated' }, // ❌ Invalid
  { id: 'payment_intent.succeeded' }, // ❌ Invalid, Stripe's name
];

schedules: [{ id: 'daily-reconcile' }, { id: 'hourly-check' }];
```

Duplicate ids within one definition are rejected. Collisions across definitions are caught by `validateIntegrationDefinitions()` at registration.

## Storage keys

A key is `"<dataType>"` or `"<dataType>/<subtype>"`, lowercase `[a-z0-9_-]`, with at most one `/`. The data type (the part before `/`) must be one the scope allows:

| Scope | Allowed data types |
| - | - |
| `account` | `activation`, `settings`, `metrics_summary`, `sync_state`, `cache` |
| `user` | `config`, `preferences`, `cache` |

`activation` is readable but never writable or deletable through `ctx.storage` — the host owns activation rows. `secret` is reserved in both scopes; use `ctx.secrets` instead. A subtype is at most 100 characters.

```ts theme={null}
// ✅ Valid account keys
'settings';
'settings/location';
'cache/forecast';
'sync_state/cursor';

// ✅ Valid user keys
'preferences/units';
'config';

// ❌ Invalid
'Settings'; // capital
'cache/forecast/daily'; // two slashes
'cache.forecast'; // dot
'config'; // in the account scope: not an allowed account data type
'secret/api-key'; // reserved data type
```

## Cron expressions

Standard 5 fields: `minute hour day-of-month month day-of-week`

| Field | Range | Allowed |
| - | - | - |
| Minute | 0-59 | `*`, lists `1,15`, ranges `1-5`, steps `*/15` `0-30/5` |
| Hour | 0-23 | same |
| Day of month | 1-31 | same |
| Month | 1-12 | same |
| Day of week | 0-7 | same (0 and 7 both Sunday) |

```ts theme={null}
// ✅ Valid
'0 6 * * *'; // 6 AM daily
'0 9 * * 1-5'; // 9 AM weekdays
'*/15 * * * *'; // Every 15 minutes
'0 0 1 * *'; // 1st of month midnight

// ❌ Invalid
'0 6 * * * 0'; // 6 fields (includes seconds)
'0 6 * * MON'; // Named day (MON not allowed)
'@daily'; // Macros not allowed
'5/15 * * * *'; // Step on a single value — use */15 or a range like 0-59/15
```

Validation happens at import time.

## Egress entries

HTTPS origin: scheme, host, optional port. No path, query, fragment, credentials.

```ts theme={null}
// ✅ Valid
'https://api.example.com';
'https://api.example.com:8443';
'https://*.cdn.example.com';
'https://*.api.example.com:9000';

// ❌ Invalid
'http://api.example.com'; // http not allowed
'https://api.example.com/v1'; // path not allowed
'https://api.example.com?key=val'; // query not allowed
'https://*.*.example.com'; // multiple wildcards
'https://user:pass@api.example.com'; // credentials not allowed
```

Wildcards are a single leading `*.` only. They match subdomains at any depth, but never the apex — list the apex separately if you call it:

```ts theme={null}
'https://*.example.com'; // Matches a.example.com and a.b.example.com
// Does NOT match example.com
```

Scheme and port must match exactly; a wildcard never crosses either. Default ports are normalized, so `https://api.example.com:443` and `https://api.example.com` are the same origin.

## OAuth endpoints

Must be absolute HTTPS URLs and covered by your egress allowlist:

```ts theme={null}
provider: {
  authorizationUrl: 'https://oauth.provider.com/authorize',  // ✅ Absolute
  tokenUrl: 'https://oauth.provider.com/token',              // ✅ Absolute
}

egress: [
  'https://oauth.provider.com',  // ✅ Covers both
]

// ❌ Invalid
authorizationUrl: '/authorize'   // Not absolute
authorizationUrl: 'http://...'   // Not https
authorizationUrl: 'https://user:pass@oauth.provider.com/authorize' // Embedded credentials
```

## Semantic names

Use clear, descriptive names that tell the reviewer what you do:

```ts theme={null}
tools: [
  'list_payments', // ✅ Clear
  'get_payment_by_id', // ✅ Clear
  'do_stuff', // ❌ Vague
];

webhooks: [
  { id: 'payment-completed' }, // ✅ Clear
  { id: 'event-1' }, // ❌ Vague
];
```

## Validation timing

Validation happens at **import time**, and the host re-validates against the same schema at registration:

```ts theme={null}
// Throws as soon as the module loads
export const integration = defineIntegration({
  slug: 'INVALID SLUG',
  name: 'Example',
  description: 'Example.',
  version: '1.0.0',
});
```

The thrown message lists every issue by path:

```
Invalid integration definition for "INVALID SLUG":
  - 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.
```

Cron syntax and OAuth egress coverage are checked right after the schema parse, in a second pass with the same message shape.

## Next steps

* [Integration shape](/sdk/integration-shape) — full definition structure
* [Testing](/sdk/testing) — test your definitions


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