# Skills

Skills are AI processes owned by the tenant. A Skill has an immutable active
version (what runs) and an optional draft (the working copy). skills.get
returns both. skills.rename changes the name at once, without a draft;
everything else (summary, instructions, inputs, outputs, scope, integrations,
scripts, settings) goes through the draft.

## Flow

1. `skills.create` opens a draft, empty, with content, or from a template
   found with `skills.search`. Each user holds one unsaved draft per kind; a
   call with different content fails with `skill_draft_exists`.
2. `skills.draft.save` replaces the whole working copy. Start from
   `skills.get` and send every field that should remain, changed or not; send
   the current `draft_revision` as `expected_revision`. The response carries
   `issues`, the same list `skills.validate` returns.
3. `skills.validate` checks the draft (or the published version when no draft
   exists) exactly like a publish and returns `valid` plus every issue with
   `path`, `code`, `message`, `hint` and `blocking`. Fix every blocking issue
   with another `skills.draft.save`, then validate again.
4. `skills.integration_bindings.list` with `skill_id` and `space_id` shows the
   integration requirements and which of them the caller has bound; before
   the first publish it reads them from the draft (`source: "draft"`).
5. `skills.save` publishes: it validates again, stores a new immutable
   version, makes it active and clears the draft. MCP clients may call it;
   in the in-app assistant it asks the user for approval. Publish only when
   the user asked for it, never as a follow-up to a rename or a draft save;
   otherwise report that the draft is still unpublished.
6. `skills.integration_bindings.set` binds each requirement to one of the
   caller's own active Space grants. It needs a published version.
7. `skills.automation.set` stores the triggers and the run request of the
   Skill in a Space. Active triggers need a published version.
8. Webhook test: while the webhook is inactive,
   `skills.automation.webhook.listen` captures the next authenticated delivery
   within 10 minutes instead of running it; `skills.automation.get` then shows
   it under `webhook.sample.payload`, the item a script for this trigger
   receives (body is the provider object).

When changing a published Skill the version already exists, so bindings and
automation can be set before the next `skills.save`.

Other operations: `skills.draft.discard` drops the working copy of a Skill
that has an active version (a never-published Skill is deleted with
`skills.delete` instead). `skills.restore` creates a new active version from
an earlier one and drops the draft; it asks the user for approval.
`skills.versions` lists versions. `skills.delete` is irreversible and fails
while runs exist.

Running: `skills.runs.start` starts one run in a Space with inputs and
documents; runs cost credits. `skills.runs.get` and `skills.runs.events`
(cursor) show status, progress and results; `skills.runs.cancel` stops a run.
`skills.models` lists the AI models a Skill may use.

## Rules

- `instructions` and `capabilities` are required for a publish.
  `capabilities` is explicit: list every capability the Skill uses. Known
  capabilities: browser, email, folders, inbox, integrations, items-output, pages, persistent-output, records, reviewer, sheet-output, web-search.
- Records: a Skill that reads or writes records lists `records` in
  `capabilities` and the record type ids in `scope.recordTypeIds`
  (`records.types.list` returns them). Scripts reach only record types in
  that scope.
- `steps` no longer exists and is rejected; write the order as numbered lines
  in `instructions`.
- The definition model is strict: unknown fields fail on every level. Inputs
  need `key` (lowercase letters, digits, `_`), `type` (`text` or `document`)
  and `label`; outputs need `key`, `type` (`doc`, `sheet`, `page` or `items`)
  and `nameTemplate`; keys are unique. Integrations need `key`, `providerKey`
  and `operations` of `{resource, operation}`; scripts need `scriptId`, an
  explicit `version` and an `alias`. `entry: {"kind": "script", "alias"}`
  needs that alias under `scripts` and exactly one output of type `items`.
- Field bindings under `integrations[].operations[].fields` are `fixed`
  (`value`) or `choice` (`values`); they are checked against the provider's
  operation contract.
- Integration requirements are logical: the definition never stores grants
  or connections. Each user binds them personally per Space; unbound
  requirements block runs and script entries.
- Folder and document ids come from `documents.folders_list` and
  `documents.list`; never invent ids.
- `skills.automation.set`: a run request (`run` with scope, inputs or an
  instruction) needs at least one trigger after the call. Omit `webhook` to
  keep the current one; `null` removes it. `run.scope.record_type_ids` needs
  `records` in the Skill's capabilities.
- A publish checks: name and summary present, the definition model, field
  bindings, the pinned scripts (they belong to the Skill, the version exists,
  the static check passed, slots and operations match the Skill's
  integrations, record types lie in the scope, every `@script:<alias>` in the
  instructions is pinned) and the settings the platform offers
  (`agentScriptsEnabled`, `subagentsEnabled`, `securityReviewEnabled` needs
  the plan feature). An outdated pinned version, a missing dry run and record
  types outside a declared record scope are hints and do not block.
- Importing a Skill from another tool: map what the draft supports
  (instructions, inputs, outputs, scope, integration requirements, settings);
  ids, bindings and fields of the other system do not transfer. List in the
  answer what was left out.

## Automation

Up to three triggers per Skill and Space: a schedule (kind `rule`, `cron` or
`once`; `workflows.schedule.preview` shows the next run times), a platform
event and an inbound webhook. Each trigger has an entry: `{"kind": "agent"}`
starts the Skill agent with the stored run request; `{"kind": "script",
"alias", "parameters", "escalate"}` runs one pinned script without a model
call and, with `escalate` true, starts the agent only when the script fails or
returns exceptions. A script entry needs the caller's own integration
bindings. A webhook (auth `none`, `secret_header`, `hmac_signature`, or
`integration` with a provider such as Shopify in mode `subscription` over a
grant, or `manual`) receives as soon as it is configured and executes only
while `active`. The webhook secret is never shown in chat; the user reads it
in the Skill's automation panel. The result of `skills.automation.set` is the
effective automation, the same shape as `skills.automation.get`.

## Errors and fixes

- `validation_error` from `skills.save`: `details` maps each definition path
  (for example `outputs.0.nameTemplate`, `capabilities`, `scripts.1`) to its
  message. Call `skills.validate` for the full list with hints, fix the draft,
  validate again.
- `script_target_out_of_scope`: a pinned script reads or writes record types
  the Skill cannot reach. Blocking when `capabilities` lacks `records` and
  `scope.recordTypeIds` is empty: add both. A hint when the record types are
  only missing from `scope.recordTypeIds`: add their ids.
- `skill_draft_exists` (409) from `skills.create`: you already hold an
  unsaved draft. Read it with `skills.get(details.skill_id)` and continue it
  with `skills.draft.save`, or delete it with `skills.delete`.
- `draft_revision_conflict` (409): the draft changed elsewhere. Read it again
  with `skills.get` and retry with the current `draft_revision`.
- `automation_run_scope_requires_trigger`: `run` carries a scope, inputs or
  an instruction, but schedule, event and webhook are all missing. Send a
  trigger in the same call, or send `run` with empty inputs and scope to
  remove the automation.
- `skill_not_saved` (409): the operation needs a published version. Call
  `skills.save` first.
- `webhook_mode_invalid` (409): the webhook's auth mode does not fit the call,
  for example a signing secret for a webhook that is not a manually
  registered provider webhook, or a secret for auth `none`.
- `integration_grant_missing` (in `details.webhook`): a subscription webhook
  names no active grant of this Space. Create or pick one with
  `integrations.grants.list` or `integrations.grants.create` and pass its id as
  `webhook.integration.grant_id`.
- `integration_binding_required`: a script entry or run needs the caller's
  bindings. Bind them with `skills.integration_bindings.set`.

## Example

A Skill that receives Shopify orders by webhook and writes them to records
through a script. The script `orders` was created with `scripts.create`,
tested with a dry run and is pinned in version 3.

```json
{
  "name": "Order sync",
  "summary": "Writes every new Shopify order into the Orders records.",
  "instructions": "1. Run @script:orders with the delivered orders.\n2. When the script reports exceptions, check each order and explain what is missing.",
  "capabilities": ["integrations", "items-output", "records"],
  "scope": {"recordTypeIds": ["<orders record type id>"]},
  "integrations": [
    {
      "key": "shop",
      "providerKey": "shopify",
      "operations": [{"resource": "orders", "operation": "get"}]
    }
  ],
  "outputs": [
    {"key": "orders", "type": "items", "nameTemplate": "Synced orders"}
  ],
  "scripts": [
    {"scriptId": "<script id>", "version": 3, "alias": "orders"}
  ],
  "entry": {"kind": "script", "alias": "orders", "escalate": true}
}
```

Send it as the `draft` of `skills.draft.save`, check it with
`skills.validate`, publish with `skills.save` (the same fields, with name,
summary, categories and tags as separate arguments and the rest as
`definition`), bind `shop` with `skills.integration_bindings.set`, then:

```json
{
  "space_id": "<space id>",
  "skill_id": "<skill id>",
  "schedule": null,
  "event": null,
  "webhook": {
    "auth": "integration",
    "integration": {
      "provider_key": "shopify",
      "mode": "subscription",
      "grant_id": "<grant id>",
      "events": ["orders/create"]
    },
    "active": false,
    "entry": {"kind": "script", "alias": "orders", "escalate": true}
  },
  "run": {"inputs": {}, "scope": {"record_type_ids": ["<orders record type id>"]}}
}
```

Capture a test delivery with `skills.automation.webhook.listen`, check
`webhook.sample.payload` in `skills.automation.get`, and let the user switch
the webhook to active.
