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

# Operation contracts

> How an integration states what its operations accept and return, how an experience pins them, and what counts as a breaking change.

An integration exposes **operations**: its tools (`tool/<name>`) and its actions (`action/<name>`). The **contract** is the sealed, machine-readable statement of them. It is a `contract.json` at the integration's root, derived from your `defineIntegration` result, listing for each operation its input and output as JSON Schema, whether it reads or writes, an example of its output, and the connection it needs.

An [experience](https://sdk.ekanos.dev/guides/experiences.md) never imports your code. It pins your contract, and the pinned contract is all it is typed and checked against. That is why a change to the contract has rules.

## Derive the contract

First, declare a contract version in the integration's `ekanos.json`. The scaffold does not write one:

```json theme={null}
{
  "kind": "integration",
  "slug": "acme-cms",
  "entry": "src/integration.ts",
  "contract": { "version": "0.1.0" }
}
```

Then describe each operation completely. For the contract an operation needs:

* a `description` (required);
* a typed output, so an experience can bind to it: `output` as a zod schema on a tool or an action (without one the output is **opaque** and an experience cannot use the operation);
* whether it reads or writes: `effect: 'read' | 'write'`. An operation with no `effect` is treated as a write, with a warning;
* an example of its output, for the harness and for tests: `outputExample`, or a recorded one (below). A read operation without one is an error.

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

const postList = z.object({
  items: z.array(z.object({ id: z.string(), title: z.string() })),
});

export const integration = defineIntegration({
  // ...
  tools: [
    {
      name: 'list_posts',
      description: 'List recent posts.',
      parameters: {
        type: 'object',
        properties: { limit: { type: 'number' } },
        required: [],
        additionalProperties: false,
      },
      effect: 'read',
      output: postList,
      outputExample: { items: [{ id: 'p_1', title: 'Hello' }] },
      async run(ctx) {
        const response = await ctx.fetch('https://api.acme.example/v1/posts');
        return response.json();
      },
    },
  ],
  actions: {
    'create-draft': {
      description: 'Create a draft post.',
      input: z.object({ title: z.string().min(1) }),
      output: z.object({ id: z.string() }),
      effect: 'write',
      async handler(ctx, input) {
        const response = await ctx.fetch('https://api.acme.example/v1/drafts', {
          method: 'POST',
          body: JSON.stringify(input),
        });
        return response.json();
      },
    },
  },
});
```

Run the command from the integration's folder:

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

It loads your definition, derives the contract with `deriveContract` from `@ekanos/sdk/contract`, and writes `contract.json`. Commit it: it is reviewed like code, and it ships in the published package.

## The three modes

**--check** `boolean`

Derives in memory and **fails if `contract.json` differs**. It writes nothing.
Use it in CI and as the integration's `prepack` script: `"prepack": "ekanos
    contract --check"`. The `checks/contract-emit` stage of `ekanos validate`, and
so of `ekanos publish`, makes the same freshness check for an integration that
declares a contract version.

**--record** `boolean`

Fills in missing output examples under `contracts/examples/`. Without `--op`,
it writes a **placeholder** synthesized from the output schema for each
operation that has no example. The command says so, and the harness serves
those placeholders in fixtures mode until you replace them. With `--op`, it
records a **real** example from a run (below).

**--bump** `boolean`

Rewrites `contract.version` in `ekanos.json` to the smallest version that
satisfies the rules for this change.

Recording a real example from a run:

```bash theme={null}
ekanos contract --record --op tool/list_posts                 # from fixtures (the default)
ekanos contract --record --op tool/list_posts --from live     # from a live run
ekanos contract --record --op tool/list_posts --input '{"limit":5}'
```

`--op` takes one or more operations, comma separated. `--input` takes JSON for a single `--op`; without it the input is synthesized from the input schema. A recording is checked against the output schema before anything is written, and live data may contain real records, so review the file before you commit it. **A write is never recorded live.** To record a write, add a scenario fixture at `fixtures/<integration>/action-create-draft.default.json` holding a real-shaped response, then record it with `--from fixtures`; nothing is called.

`--check` cannot be combined with `--record` or `--bump`.

## Versions and compat lines

A contract has its own version, `contract.version`, independent of the package version, and a **compat line** derived from it:

| Contract version | Compat line |
| - | - |
| `0.2.0`, `0.2.5` | `0.2` |
| `1.4.0` | `1` |

For a 0.x contract the line is `0.<minor>`; from 1.0 it is the major. An experience pins a line (`"version": "0.2"` in its `ekanos.json`), so everything on that line has to stay compatible with what the experience was built against. Operations carry the line in their id: `acme-cms@0.2/tool/list_posts`.

## What is a breaking change

Every change between two contracts is classified. Consumers **send** inputs and **read** outputs, so the same edit is judged in opposite directions on each side.

| Surface | Breaking | Not breaking |
| - | - | - |
| Inputs | Add a required field. Narrow a type. Remove an enum value. Remove or rename a field. Add a required scope. Change the auth type | Add an optional field. Widen a type. Add an enum value |
| Outputs | Remove or rename a field. Add an enum alternative. Widen a type. Change an operation from read to write | Add an optional field. Add a required field. Narrow a type |
| Events | The same rules as outputs. Remove an event | Add an event |
| Operations and connection | Remove an operation. Remove a connection level | Add an operation. Add a connection level. Remove a scope. Narrow an effect from write to read |
| Opaque members | Any change | None |

Descriptions, titles, defaults, examples, and whether an object allows extra keys are ignored: documentation is not a contract, and a consumer's tolerance of extra response keys is what makes "add an optional output field" safe.

## What `ekanos contract` enforces

* A **breaking change on the same compat line is an error**. Start a new line instead: on a 0.x contract, bump the minor (`0.2.0` to `0.3.0`); from 1.0, bump the major.
* **Any change needs a higher `contract.version`.** If the surface changed and the version did not, the command fails. If the version went down, it fails.
* From 1.0, an **additive change needs a minor bump**. On a 0.x contract the minor is the line, so an additive change is a patch bump.
* A change to the package only (no change to the operations) leaves the contract untouched.

`ekanos contract --bump` works out the right bump for you and writes it to `ekanos.json`. A breaking change that opens a new line is allowed: that is what a line is for. Experiences pinned to the old line keep working against it.

## Pins in an experience

`ekanos add acme-cms@0.2` copies the contract it found into the experience's `contracts/` folder and records its digest in `ekanos.lock.json`. From then on:

* `ekanos check` fails if the pinned line has no such operation, or if the stored contract no longer matches the lock;
* `ekanos sync` accepts a new digest **within** the pinned line (that is, compatible changes), and `ekanos sync --check` reports drift without writing;
* moving to a new line is a deliberate edit to `version` in `ekanos.json`, followed by `ekanos sync`.

## Next steps

* [Build an experience](https://sdk.ekanos.dev/guides/experiences.md)
* [Publishing from a repo](https://sdk.ekanos.dev/cli/publishing-a-repo.md)


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