<!-- Source: https://docs.squirro.com/en/latest/technical/neo/admin-guide/agent-config/api-reference.html -->
# API Reference

> **Warning**
>
> Project Neo is currently in [Technical Preview](../../../../a-z/squirro-glossary.md#term-Technical-Preview). Features described in this section may change before general availability.

The HTTP API is the surface that the `genai` CLI wraps. Use it directly for browser-side clients and programmatic deployment workflows. For the command line client, see the [CLI Reference](cli-reference.md#neo-agent-config-cli) page.

The API is small. Every route is scoped to a Squirro project, the platform project identified by the `{project_id}` in the path. That value appears in the address bar after `/neo/` when you open a project in Project Neo. The administrative routes are addressed as `/admin/projects/{project_id}/...`, and the scheduled task routes as `/projects/{project_id}/tasks/...`. The paths below are shown relative to the GenAI service base. A deployed instance serves that base at `/service/genai/v1`, so prepend it to each route:

```text
https://<squirro-cluster>/service/genai/v1/admin/projects/{project_id}/deploy
```

Every response that carries a body carries JSON, and the one route that returns none is the task delete, which answers `204`.

The routes are reachable from outside the cluster, over the same host that serves the web interface, and every call is authenticated as described in the next section. Whether the service is exposed depends on the web server configuration of your deployment, so if a request does not reach the service at all, check with whoever administers the instance. For the GenAI service API as a whole, of which these routes are one part, see the [GenAI service API](../../../api/squirro_client/sc-api-genai.md#sc-api-genai) page.

## Authentication and Authorization

The configuration routes are all gated by one access check, resolved against the project ID in the path. The caller must hold the project-level Administrator role on that Squirro project. A server-level Administrator qualifies as well, because that role carries Administrator rights on every project. The check is project-scoped: a role granted on one project does not carry to another. For how these roles are defined and assigned, see the [Squirro Roles & Permissions](../../../ui/permissions.md#ui-permissions) page.

A request from a caller without the Administrator role on the project receives a `403` response. The authenticated user becomes the actor recorded on deploy receipts.

The scheduled task routes are the exception. They ask only for read access to the project, and every one of them is scoped to the calling user, who sees and acts on their own tasks alone. A task created by one user is not visible to another, and an Administrator does not gain access to the tasks of others through these routes.

Read access to the project is checked before a request reaches the service, and the Administrator role is checked by the service itself. Both answer `403`, so a caller who can read the project but is not an Administrator is refused, in that second check, for the reason given above.

### Authenticating a Request

These routes authenticate the same way as the rest of the GenAI service API, with a Squirro access token in the `Authorization` header. For the header itself, and for the case where users are authenticated through an external identity provider, see the [GenAI Service API](../../../api/services/genai/index.md#genai-api) page. For what an access token is and how to obtain one, see the [Authentication](../../../api/authentication.md#api-authentication) page.

The point worth stressing here is that a refresh token, which is what the API Access section of the Squirro settings gives you, does not work in that header. Exchange it for an access token first. Alongside the platform exchange described on the pages above, the GenAI service accepts the exchange on a route of its own, which saves a browser-side client from calling the User API separately:

```text
POST /service/genai/v1/auth/token
Content-Type: application/json

{ "refresh_token": "..." }
```

The response carries the access token alone:

```text
{ "access_token": "..." }
```

This route takes no `Authorization` header, since the refresh token in the body is the credential. A browser-side client that would otherwise be blocked by the browser origin rules can add a `cluster_url` field naming its own origin. Leave the field out from anywhere else, including from a script running outside the cluster, and the service uses the User API it is configured with.

An access token is short-lived, as noted on the [Authentication](../../../api/authentication.md#api-authentication) page, while a refresh token is not. A long-running client therefore keeps the refresh token, exchanges it for an access token, and exchanges again when a call answers `401`. This is what the `genai` CLI does, which is why it asks for a refresh token once and does not ask again on every command.

Putting the pieces together, a deploy is:

```text
curl -X POST \
  "https://<squirro-cluster>/service/genai/v1/admin/projects/{project_id}/deploy" \
  -H "Authorization: Bearer <access token>" \
  -H "Content-Type: application/json" \
  -d '{"resources": [ ... ], "labels": {"git.hash": "abc1234"}}'
```

## Deploy a Snapshot

```text
POST /admin/projects/{project_id}/deploy[?dry_run=true]
Content-Type: application/json
```

Publishes a new configuration snapshot for the project. With `?dry_run=true`, the server runs every check it would run for a real deploy without writing the snapshot.

The request body carries the full configuration:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `resources` | list | yes | Every envelope-bearing resource, in any order: exactly one `Project`, zero or more `Agent` resources with their instructions inline, and the other resource kinds. The server dispatches by envelope. |
| `labels` | object | no | Flat key and value metadata recorded on the deploy receipt. Up to 64 entries, with bounded key and value lengths. |

The server runs every publish check over the submitted payload. On success, the payload replaces the current snapshot in a single all-or-nothing switch: either the whole configuration becomes live or nothing changes.

A successful response carries the deploy receipt:

```text
{
  "content_hash": "sha256:...",
  "labels":       { "git.hash": "abc1234" },
  "timestamp":    "2026-05-14T11:32:00+00:00",
  "actor":        "alice@example.com",
  "dry_run":      false
}
```

The `content_hash` is a hash over the validated snapshot. Two deploys with identical content produce identical hashes. On a dry run the snapshot is validated but not written, and the response marks itself as a dry run, with `content_hash` set to the placeholder `(dry-run)` because no snapshot was stored to hash.

## Read Deploy Status

```text
GET /admin/projects/{project_id}/status
```

Returns the deploy receipt for the current snapshot without transferring the snapshot itself. Returns `404` when the project has no snapshot deployed.

```text
{
  "content_hash": "sha256:...",
  "labels":       { "git.hash": "abc1234" },
  "timestamp":    "2026-05-14T11:32:00+00:00",
  "actor":        "alice@example.com"
}
```

This is the deploy receipt without the `dry_run` field, which only a deploy or a reseed response carries. The `timestamp` is the time of the deploy that produced the current snapshot, not the time of your request, so an unchanged `content_hash` and `timestamp` across two calls mean nothing has been deployed in between.

## Read Deploy History

```text
GET /admin/projects/{project_id}/log[?since=...&until=...&actor=...]
```

Returns the deploy history for the project, most recent first, as an `entries` list rather than a bare list. Each entry has the same shape as the status response.

| Parameter | Type | Notes |
| --- | --- | --- |
| `since` | RFC 3339 timestamp | Include only entries at or after this time. |
| `until` | RFC 3339 timestamp | Include only entries at or before this time. |
| `actor` | string | Include only entries where the actor matches exactly. |

All parameters are optional and combinable.

```text
{
  "entries": [
    {
      "content_hash": "sha256:...",
      "labels":       { "git.hash": "abc1234" },
      "timestamp":    "2026-05-14T11:32:00+00:00",
      "actor":        "alice@example.com"
    },
    {
      "content_hash": "sha256:...",
      "labels":       { "git.hash": "9f2e5d1" },
      "timestamp":    "2026-05-09T08:47:19+00:00",
      "actor":        "bob@example.com"
    }
  ]
}
```

The history holds receipts alone, so it tells you what was deployed and by whom, not what the configuration contained.

## Read the Current Snapshot

```text
GET /admin/projects/{project_id}/snapshot
```

Returns the current snapshot as a flat list of envelope-bearing resources, suitable for writing to disk. Backs `genai export`. Returns `404` when the project has no snapshot deployed.

The response has the same shape as the `resources` field of a deploy body, which is what lets an exported snapshot be redeployed unchanged. Agent instructions are carried inline, so no file is left behind.

```text
{
  "resources": [
    {
      "kind": "Project",
      "schema": "v1",
      "title": "My first project",
      "prefers": { "inference.model": "claude-opus" }
    },
    {
      "kind": "Model",
      "schema": "v1",
      "metadata": { "id": "claude-opus" },
      "provider": "anthropic",
      "model_name": "claude-opus-4-8",
      "auth": { "type": "bearer", "secret": "anthropic_token" }
    },
    {
      "kind": "Agent",
      "schema": "v1",
      "metadata": { "id": "assistant" },
      "entrypoint": true,
      "instructions": "You are a helpful assistant. Answer questions clearly and concisely."
    }
  ]
}
```

The `Project` resource comes first, then the other resource kinds, then the agents. Every field of every resource is serialized, including the ones left at their default, so a real response is longer than the extract above.

> **Note**
>
> This route always returns the current snapshot. There is no parameter to retrieve a past snapshot by content hash. `GET .../log` returns only the receipt for a past deploy, not its resources. To restore a previous configuration, redeploy it from version control, as described in the [Roll Back a Deployment](cli-reference.md#neo-agent-config-rollback) section.

## Read the Picker Payload

```text
GET /admin/projects/{project_id}/resources
```

Returns the picker payload: the description an interface fetches once at startup so it can display the settings a user is allowed to change, and offer only the values each setting accepts. It gives the settings that apply across the project, then, for each agent a user can select, only the settings where that agent differs.

Pins appear as a fixed value, constraints narrow the set of allowed values, and preferences appear as the default value. Per-request information about which source supplied which value does not appear on this surface, which describes only what a user may choose.

The payload has four top-level fields. `project` names the project, `resources` lists the catalogs a user picks from, `baseline.params` carries those project-wide settings, and `assistants` lists the selectable agents, each with the settings where it differs from `baseline`.

Note that `resources` means something different here from the `resources` list of a deploy body or a snapshot response. On this route it is the set of catalogs a user chooses from, such as the available models, and not the deployed resource files.

```text
{
  "project": { "title": "My first project", "summary": "" },
  "resources": {
    "models": [
      {
        "id": "claude-opus",
        "title": "Claude Opus",
        "summary": "",
        "enable_reasoning": true,
        "enable_image_input": true
      }
    ],
    "mcp_servers": [],
    "knowledge_graphs": [],
    "trait_dimensions": [
      {
        "id": "style",
        "title": "Style",
        "summary": "",
        "traits": { "concise": { "title": "Concise", "summary": "" } }
      }
    ]
  },
  "baseline": {
    "params": {
      "inference.model": {
        "type": "string",
        "title": "Model",
        "description": "Model used for generating responses.",
        "enum": ["claude-opus"],
        "x-catalog": "models",
        "default": "claude-opus"
      },
      "source.documents.enabled": {
        "type": ["boolean", "string"],
        "title": "Documents",
        "description": "Use document retrieval for this conversation.",
        "enum": [true, false, "auto"],
        "default": "auto"
      },
      "trait.style": {
        "type": ["string", "null"],
        "title": "Style",
        "enum": [null, "concise"]
      }
    }
  },
  "assistants": [
    {
      "id": "assistant",
      "title": "Assistant",
      "summary": "",
      "opening": null,
      "pin_to_conversation": false,
      "overlay": {
        "params": {
          "trait.style": {
            "type": ["string", "null"],
            "title": "Style",
            "enum": [null, "concise"],
            "const": "concise"
          }
        }
      }
    }
  ]
}
```

Each entry under `params` is a JSON Schema fragment, so a form renderer consumes the payload as it stands. `enum` is the set of allowed values, narrowed by any constraint. `default` is the resolved preference. `const` is a fixed value, which is what a pin becomes: the extract above shows an agent that pins `trait.style` to `concise`, so it appears in that agent overlay and not in the baseline. `x-catalog` names the catalog under `resources` that the allowed values come from.

An overlay lists only the paths whose fragment differs from the baseline, and it carries the whole fragment for each. The form for one agent is therefore its overlay entry for a path where it has one, and the baseline entry otherwise.

A toggle whose backing resource kind has nothing published loses `true` from its `enum` and carries `"x-ui-hint": "capability_unavailable"`, so an interface can explain why the switch cannot be turned on. This route reports no per-user authentication state for MCP servers, so every entry under `mcp_servers` reports `authenticated: false`.

## Rebuild From the Bundle

```text
POST /admin/projects/{project_id}/reseed[?dry_run=true]
Content-Type: application/json
```

Rebuilds the bundled-workspace snapshot for the project and re-deploys it. The request body is optional and accepts two fields: `force` (boolean, default `false`) and `labels` (the same flat key and value metadata as a deploy, recorded on the resulting receipt).

By default the server refuses with a `409` response to overwrite a snapshot that was deployed by an operator. Set `force` to `true` to override that guard. A project with no current snapshot is seeded unconditionally.

With `?dry_run=true`, the server rebuilds the bundled snapshot and runs every check without writing it. When the project does not have a valid bundled workspace, the server responds with `500` and an error body identifying the workspace and the failures.

This route is available only when the platform ships a bundled workspace for the project. For what a bundled workspace is and when to reseed, see the [genai reseed](cli-reference.md#neo-agent-config-bundled-workspace) section on the CLI reference page.

## Configuration Resolution

A resolve route reports how each setting resolves for a given agent, user, and request, and it backs `genai explain`. It is a development-time facility rather than part of the surface a Squirro instance serves: it ships with the development application of the configuration library, which serves it alongside the administrative routes, whereas a deployed instance serves the administrative and task routes only.

Point tooling that depends on resolution at a local run of that application, and treat what it reports as a debugging aid rather than something to build a product on. The four sources it reports on are described on the [Overview](overview.md#neo-agent-config-overview) page.

## Scheduled Tasks

These routes back the `genai task` commands. They manage scheduled tasks, which are not part of the deployed configuration. For what a task is and how approval rules work, see the [Scheduled Tasks and Approvals](tasks-and-approvals.md#neo-agent-config-tasks) page.

### List and Create Tasks

```text
GET  /projects/{project_id}/tasks[?limit=50&offset=0&sort=updated_at&order=desc]
POST /projects/{project_id}/tasks
Content-Type: application/json
```

`GET` returns the tasks of the calling user in the project, wrapped in the pagination fields shown in the list response below.

| Parameter | Type | Notes |
| --- | --- | --- |
| `limit` | integer | Between 1 and 100. Defaults to 50. |
| `offset` | integer | Zero or greater. Defaults to 0. |
| `sort` | string | One of `created_at`, `updated_at`, `name`, `next_execution_at`, or `last_execution_at`. Defaults to `updated_at`. Any other value returns `400`. |
| `order` | string | `asc` or `desc`. Defaults to `desc`. |

`POST` creates a task and responds with `201` and the created task.

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `name` | string | yes | Between 1 and 100 characters. |
| `instruction` | string | yes | What the agent is asked to do on each run. |
| `trigger` | object | yes | When the task fires. See the trigger table below. |
| `enabled` | boolean | no | The task-level switch. Defaults to `true`. |
| `email_notifications_enabled` | boolean | no | Defaults to `false`. |
| `hitl` | object or null | no | Approval rules for this task, as `{"rules": [{"pattern": ..., "disposition": ...}]}`. Omitted means no rules, so a tool that needs approval is refused on a scheduled run. |

The `trigger` object carries a `type` that selects its shape. Today the only type is `schedule`, and its `enabled` field controls the schedule alone, independently of the task-level `enabled`.

```text
{
  "name":        "Nightly document digest",
  "instruction": "Summarize the documents added today and post the summary.",
  "trigger": {
    "type":    "schedule",
    "enabled": true,
    "schedule": {
      "time":       "02:00",
      "timezone":   "Europe/Zurich",
      "recurrence": "daily"
    }
  }
}
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `time` | string | yes | Local time of day, as `HH:MM` or `HH:MM:SS`. |
| `timezone` | string | yes | An IANA timezone name, such as `Europe/Zurich`. |
| `recurrence` | string | yes | `daily`, `weekly`, `monthly`, or `annually`. |
| `days_of_week` | list of integers | for `weekly` | Non-empty, with 0 for Sunday through 6 for Saturday. |
| `day_of_month` | integer | for `monthly` and `annually` | Between 1 and 28 for `monthly`. For `annually`, within the length of the chosen month, with February capped at 28 so the date exists every year. |
| `month_of_year` | integer | for `annually` | Between 1 and 12. |

A task carries the identity of the user who created it, and each run acts as that user. The number of tasks one user may hold in a project is capped, and a create beyond that cap returns `409`.

A list response looks like this:

```text
{
  "tasks": [
    {
      "id": "3f9a1c7e-5b2d-4e18-9c0a-71d4e2f6b8aa",
      "name": "Nightly document digest",
      "instruction": "Summarize the documents added today and post the summary.",
      "enabled": true,
      "trigger": {
        "type":    "schedule",
        "enabled": true,
        "schedule": {
          "time":          "02:00",
          "timezone":      "Europe/Zurich",
          "recurrence":    "daily",
          "days_of_week":  null,
          "day_of_month":  null,
          "month_of_year": null
        }
      },
      "email_notifications_enabled": false,
      "hitl": { "rules": [ { "pattern": "send_email", "disposition": "defer" } ] },
      "next_execution_at":     "2026-05-15T00:00:00Z",
      "last_execution_at":     "2026-05-14T00:00:00Z",
      "last_execution_status": "succeeded",
      "conversation_count":    12,
      "created_at":  "2026-05-01T09:14:03Z",
      "updated_at":  "2026-05-14T00:00:12Z",
      "created_by":  "alice@example.com"
    }
  ],
  "total":  1,
  "limit":  50,
  "offset": 0
}
```

The task object is the same on every route that returns one task, so `POST`, `GET .../tasks/{task_id}`, and `PATCH` all answer with the object shown inside `tasks` above. The `id` is assigned by the platform and is the `{task_id}` of every other route. Timestamps are UTC. `next_execution_at` and `last_execution_at` are `null` until the schedule has a next slot and the task has run at least once, and the schedule fields that the recurrence does not use are present and `null`.

### Read, Update, and Delete a Task

```text
GET    /projects/{project_id}/tasks/{task_id}
PATCH  /projects/{project_id}/tasks/{task_id}
DELETE /projects/{project_id}/tasks/{task_id}
```

`GET` returns one task, in the shape shown under List and Create Tasks: the fields you supplied, plus the ones the platform sets for you.

`PATCH` changes only the fields present in the body. The `trigger` is the exception: it is replaced as a whole, so send the complete object to change any part of the schedule. Because the task-level `enabled` is a field of its own, pause or resume a task by sending `enabled` alone.

`DELETE` responds with `204`. The run history and the conversations produced by past runs are kept.

### Run a Task and Read Its History

```text
POST /projects/{project_id}/tasks/{task_id}/runs
GET  /projects/{project_id}/tasks/{task_id}/runs[?limit=50&offset=0]
POST /projects/{project_id}/tasks/{task_id}/runs/{run_id}/retry
Content-Type: application/json
```

`POST .../runs` fires the task immediately and responds with `201`. A manual run fires whatever `enabled` and `trigger.enabled` are set to, which lets you test a task before its schedule starts it.

`GET .../runs` returns the run history, most recent first, wrapped in the same pagination fields as a task list:

```text
{
  "runs": [
    {
      "id":                 "418",
      "task_id":            "3f9a1c7e-5b2d-4e18-9c0a-71d4e2f6b8aa",
      "status":             "succeeded",
      "trigger_source":     "scheduled",
      "conversation_id":    "b70c25d9-8f43-4a61-8e2c-0d3f9a1b6c47",
      "conversation_title": "Nightly document digest",
      "error_message":      null,
      "scheduled_for":      "2026-05-14T00:00:00Z",
      "started_at":         "2026-05-14T00:00:04Z",
      "completed_at":       "2026-05-14T00:00:12Z"
    }
  ],
  "total":  14,
  "limit":  50,
  "offset": 0
}
```

A run `id` is a number in string form, unlike the task `id`, which is a UUID. The `status` is one of `running`, `awaiting_approval`, `succeeded`, `incomplete`, `failed`, or `timed_out`, where `awaiting_approval` is a run parked on a `defer` rule and `incomplete` is a run that stopped without finishing while keeping the output it had produced. The `trigger_source` is `scheduled` or `manual`. A manual run fills no schedule slot, so its `scheduled_for` is `null`, and a run that has not finished carries a `null` `completed_at`. The same object is returned by `POST .../runs` and `POST .../retry`.

`POST .../retry` re-runs a past run as a new manual run and responds with `201`. Only a run that failed or ended without finishing can be retried, and any other run returns `409`. The optional body takes a single field, `conversation_id`, to continue an existing conversation instead of starting one.

## Error Contract

An error raised by the service carries a `detail` field. A request rejected before it reaches the service, which is the authentication case, does not.

| Status | Meaning | Detail shape |
| --- | --- | --- |
| `400` | Malformed payload, or a business-rule violation caught before validation. On a task list, an unsupported `sort` field. | A string. |
| `401` | No credentials were supplied, or the access token is invalid or has expired. Exchange the refresh token for a new access token and retry. | Produced before the request reaches the service, so read the status code rather than the body. |
| `403` | The caller does not hold the Administrator role on the project, or, on a task route, cannot read the project. | Supplied by the host application. |
| `404` | A status, snapshot, or resources request against a project with no snapshot. On a task route, a task or run that does not exist or does not belong to the caller. | A string. |
| `409` | A reseed against an operator-deployed snapshot without `force`. On a task route, a create beyond the per-user task cap, or a retry of a run that neither failed nor ended without finishing. | A string. |
| `422` | Publish checks failed on deploy, or request validation failed on another route. | On a deploy, an object of the form `{"failures": [[path, message], ...]}`. Request validation on another route uses the framework standard error shape. |
| `500` | A reseed whose bundled workspace could not be built. | An object identifying the workspace and the failures. |

A `422` on deploy is the canonical publish-rejected shape. A client should read `detail.failures` as the list of failures rather than treating the response as a single-line error. For how to read the list and fix each kind of failure, and for the common causes behind the other status codes, see the [Troubleshooting](troubleshooting.md#neo-agent-config-troubleshooting) page. When the failure is an unresolvable credential handle, register the credential as described in the [Registering a Handle](resource-reference.md#neo-agent-config-register-handle) section.
