API Reference#

Warning

Project Neo is currently in 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 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:

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 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 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 page. For what an access token is and how to obtain one, see the 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:

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

{ "refresh_token": "..." }

The response carries the access token alone:

{ "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 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:

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#

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:

{
  "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#

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.

{
  "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#

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.

{
  "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#

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.

{
  "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 section.

Read the Picker Payload#

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.

{
  "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#

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

List and Create Tasks#

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.

{
  "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:

{
  "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#

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#

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:

{
  "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 page. When the failure is an unresolvable credential handle, register the credential as described in the Registering a Handle section.