# Integrations

Integrations connect external systems (shops, CRMs, mail, accounting, databases) to
the tenant. Three objects matter:

- A **connection** holds the credentials of one external account. It always belongs
  to the user who created it; only that user can call operations through it.
- A **grant** allows one connection inside one Space: which provider operations may
  run there, with optional constraints. Skills, workflows and agents working in a
  Space use grants.
- A **provider operation** is a call to the external system, registered as
  `integrations.<provider>.<resource>.<operation>` (for example
  `integrations.shopify.orders.list`). Every call is recorded as an invocation.

## Flow

1. **Find the provider operations.** `search_operations(provider="shopify")` lists the
   operations of one provider; by default search only shows providers with a
   connection in the tenant, pass `connected_only=false` to see all.
   `integrations.catalog.get` describes a provider: `contract_digest`, auth schemes,
   and per operation the input schema, `risk_level`, `requires_space` and
   `constraint_policy`.
2. **Check the connection.** `get_context` lists the caller's connections with
   provider, `connection_id` and the number of active grants.
   `integrations.connections.get` shows status and setup state. Without a connection,
   `integrations.authorizations.begin` starts one; OAuth schemes return an
   `authorization_url` the user must open in the browser (offer it through the app,
   do not paste it into a message). Some providers need a setup step afterwards
   (`integrations.connections.setup_options`, then
   `integrations.connections.complete_setup`).
3. **Create a grant** with `integrations.grants.create`: `connection_id`, `space_id`
   and `allowed_operations` with full operation keys. `integrations.grants.list`
   shows existing grants; change one with `integrations.grants.update` instead of
   creating a second grant for the same connection and Space.
4. **Call the operation** with `invoke`. Arguments are the provider fields from
   `describe_operation` plus `connection_id`; operations inside a Space also take
   `space_id` and `grant_id`. Operations with `requires_space` always need them;
   others can be called without a Space only when the provider allows direct owner
   use. Queued operations return an `invocation_id`; poll
   `integrations.invocations.get`.
5. **Webhooks.** Provider webhooks are not created by hand. When
   `skills.automation.set` stores a webhook with `auth: integration` and
   `mode: subscription`, the platform creates the provider subscriptions for the
   listed `events` through the grant in `grant_id`. This happens asynchronously:
   `skills.automation.get` shows `subscription_state` `pending` until the provider
   confirmed and `active` afterwards. A subscription that already exists at the
   provider for the same event and address is adopted, not duplicated. The grant must
   allow the provider's webhook subscription operations.

## Rules

- Operation keys are always full keys: `integrations.shopify.orders.list`, never
  `orders.list` or `shopify.orders.list`.
- Every operation with `risk_level` `irreversible` in `allowed_operations` needs
  `risk_acknowledgements[<operation key>]` set to the provider's `contract_digest`.
  Ask the user to confirm the risk before you set it.
- A grant belongs to exactly one connection and one Space. The connection must belong
  to the caller and the caller must have access to the Space.
- `safe_constraints` has the form
  `{"version": 1, "constraints": {<operation key>: {<field>: {<rule>: <value>}}}}`.
  Only fields and rules listed in the operation's `constraint_policy` are accepted
  (allowed_values, max_length, maximum, maximum_items, minimum, required_value).
- `asset_boundaries` narrows a grant to part of the connection's setup selection; it
  cannot add values the setup did not select.
- Errors raised by the platform itself carry a message, `details` with the offending
  path and a `hint`: grant validation, input validation, missing permission for an
  operation, missing risk acknowledgement. Errors that come from the provider or the
  runner (timeouts, rate limits, provider rejections) only carry their code; the
  provider's own text is never passed through.

## Errors and what to do

| Code | What to do |
|---|---|
| `integration_grant_policy_invalid` | `details` names the entry, for example `allowed_operations[2]`; the hint shows the expected key format and the closest valid key. Fix that entry and retry. |
| `integration_invalid_input` | `details` lists each invalid argument path; unknown parameters list the allowed ones. Compare with `describe_operation`. |
| `integration_operation_not_allowed` | The connection exists but may not run this operation here: the grant does not allow it, `space_id`/`grant_id` are missing, or the setup does not offer it. Extend the grant with `integrations.grants.update` or ask the Space owner. |
| `integration_risk_acknowledgement_required` | `details` has `operation` and `contract_digest`; set `risk_acknowledgements[operation] = contract_digest` after the user confirmed. |
| `integration_setup_required` | Finish the connection setup with `integrations.connections.setup_options` and `integrations.connections.complete_setup`. |
| `integration_not_found` | The connection, grant or Space does not exist for the caller, or belongs to someone else. Check the ids in `get_context` and `integrations.grants.list`. |

## Example

Grant a Space read access to Shopify orders, at most 50 per page:

```json
{
  "operation_key": "integrations.grants.create",
  "arguments": {
    "connection_id": "<connection uuid>",
    "space_id": "<space uuid>",
    "allowed_operations": [
      "integrations.shopify.orders.list",
      "integrations.shopify.orders.get"
    ],
    "safe_constraints": {
      "version": 1,
      "constraints": {
        "integrations.shopify.orders.list": {"limit": {"maximum": 50}}
      }
    },
    "idempotency_key": "grant-shopify-orders-<space uuid>"
  }
}
```

Then list paid orders through that grant:

```json
{
  "operation_key": "integrations.shopify.orders.list",
  "arguments": {
    "connection_id": "<connection uuid>",
    "space_id": "<space uuid>",
    "grant_id": "<grant id from the response>",
    "query": "financial_status:paid created_at:>2026-09-01",
    "limit": 50
  }
}
```
