Glnc API Reference

version: 12

Expectation monitoring for automated work.

Define Check → Report Run → Respond with context. Glnc evaluates whether
recurring automated work happened on time and produced the expected result. A
Kanban work queue can carry the failure evidence and next action through the
response loop; it is not Glnc’s primary identity.

Vocabulary: the Glance web UI calls these actors Agents. This API and its
X-Entity header keep the term entity — they are the same concept (“Agent”
in the UI is entity on the wire).


Discovery and Bootstrap

GET /api/v1             → unauthenticated start-here JSON
GET /.well-known/agent.json → same discovery document, well-known path
GET /api/v1/openapi     → unauthenticated OpenAPI 3.1 document
GET /llms.txt           → LLM-oriented pointer to discovery, OpenAPI, and docs
GET /docs/api.md        → this document (agent-readable markdown)
GET /api/v1/setup       → bearer + X-Entity bootstrap for a known entity's queue/stage topology

Remote MCP (no install)

For Claude Cowork and other clients that support custom remote MCP servers, paste
https://glnc.io/mcp as the server URL. Do not install a package, run a local
process, or create an API key. The client discovers Glance OAuth, opens a browser,
and asks you to approve full-account access for that named client.

After authorization, first call getIdentity to confirm the expected Glance user
and account, then call get_monitoring_status for the account’s current health.
Monitoring tools are account-level and need no Entity. Work-queue tools require an
entity_slug argument because an Entity is acting. Delete operations remain
available for API parity and advertise destructive safety metadata; review and
approve the client’s confirmation before executing one.

Disconnecting in the client does not revoke server access. Review or revoke a
grant in Settings → MCP Connections. Revocation immediately invalidates its access
and refresh tokens without affecting API keys or other clients. If a client does
not support remote MCP and OAuth, use the REST API-key flow below as the fallback.

An agent that only knows the base URL should start with GET /api/v1 or
GET /.well-known/agent.json. Discovery distinguishes account-level monitoring
from entity-scoped queue work. For a scoped key, select permitted entities in
Settings → API Keys; a full-access key may use any existing account entity. Then
call GET /api/v1/setup with that known slug to learn its permitted queue and
stage topology. Setup validates a slug; it does not discover an unknown one.


Authentication

Glnc has two explicit API authentication modes.

Account-level

Monitoring and survey endpoints require a full-access key and one header:

Authorization: Bearer <api_key>

Do not send X-Entity to account-level endpoints. A scoped key receives 403.

Entity-scoped

Setup and work-queue endpoints require two headers:

Authorization: Bearer <api_key>
X-Entity: <entity_slug>

Generate the key in Settings → API Keys; scoped keys also select their permitted
entities there. entity_slug identifies which agent identity is acting. Full-access
keys may act as any existing entity in the account; entity-scoped keys may use
only an entity selected for that key. GET /api/v1/setup requires this value and
returns the permitted topology for it—it cannot supply a slug the caller does
not already know.

The entity may also be supplied as an entity query/body param if the header is inconvenient — the header takes precedence.


Response Envelope

Most responses share the same JSON envelope:

{ "ok": true,  "command": "...", "result": { ... }, "next_actions": [] }
{ "ok": false, "error": "...",   "command": null,   "result": null, "next_actions": [] }

JSON exceptions to the envelope:

  • GET /api/v1 and GET /.well-known/agent.json return flat discovery JSON.
  • GET /api/v1/openapi returns a bare OpenAPI 3.1 document.
  • GET /api/v1/version returns a flat object (see that endpoint below).
  • 401 Unauthorized returns only { "ok": false, "error": "Unauthorized" }.

GET /docs/api, GET /docs/api.md, and GET /llms.txt are public prose
documents, not API-envelope JSON endpoints.

HTTP status codes:

Code Meaning
200 Success
401 Missing or invalid API key
403 Caller is not allowed (e.g. not the claim holder)
404 Resource not found
409 Conflict with existing state (e.g. creating a check whose name is taken, or an unforced write to operator-owned remediation)
422 Validation error or missing required header/param
429 Concurrency cap exceeded

Tag Policy

Work item tags and monitoring check affected_systems values share one
account-level vocabulary governed by the account’s tag policy. Read
result.tag_policy from GET /api/v1/setup before assigning tags — never
guess or cache the list long-term:

"tag_policy": { "mode": "strict", "preferred_tags": ["...", "..."] }

Two modes:

  • preferred (default) — the preferred list is guidance; no tag is ever rejected.
  • strict — tags newly added by a request that are outside preferred_tags
    are rejected with 422. A strict policy with an empty preferred_tags list
    rejects nothing.

Enforcement is delta-based (grandfathering): only tags newly added by a
request are checked. Values already stored on the record pass untouched, so a
strict policy never blocks updating a record that carries legacy tags — only
introducing new out-of-list values.

A rejected request returns the standard error envelope:

{ "ok": false, "error": "Tags not permitted by this account's strict tag policy: <tags>. Allowed tags: <list>. Read GET /api/v1/setup for the current list." }

The tag curation endpoints POST /api/v1/monitoring/tags/rename and
POST /api/v1/monitoring/tags/merge honor the same policy: under strict mode,
renaming or merging into a tag outside the preferred list returns 422 unless
every affected check already carries the target tag (the same delta semantics).
POST /api/v1/monitoring/tags/delete is unaffected — removing a tag never adds
vocabulary.


Endpoints

GET /api/v1 and GET /.well-known/agent.json

Unauthenticated start-here discovery document. Both paths return the same flat
JSON shape. Use it when an agent only knows the glnc base URL and needs to learn
how to authenticate, where the OpenAPI document lives, and which core endpoints
start the workflow.

Response (whole body):

{
  "product": "Glnc",
  "category": "Expectation monitoring for automated work.",
  "workflow": "Define Check → Report Run → Respond with context.",
  "response_loop": "A Kanban work queue carries remediation context; it is not the product's primary identity.",
  "version": "12",
  "authentication": {
    "key_management_url": "https://glnc.io/settings/api_keys",
    "modes": {
      "account_level": {
        "headers": { "Authorization": "Bearer <api_key>" }
      },
      "entity_scoped": {
        "headers": {
          "Authorization": "Bearer <api_key>",
          "X-Entity": "<known entity_slug; scoped keys select it in Settings → API Keys>"
        }
      }
    }
  },
  "start_here": [
    { "method": "GET", "path": "/api/v1/setup", "auth_mode": "entity_scoped", "summary": "..." }
  ],
  "docs": {
    "full_reference": "https://glnc.io/docs/api.md",
    "openapi": "https://glnc.io/api/v1/openapi",
    "llms_txt": "https://glnc.io/llms.txt"
  }
}

GET /api/v1/openapi

Unauthenticated OpenAPI 3.1 document generated from Api::EndpointCatalog. The
body is a bare OpenAPI document, not wrapped in the glnc response envelope.


GET /llms.txt

Unauthenticated plain-text LLM pointer document. It is hand-authored and links to
discovery, OpenAPI, this API reference, and the monitoring loop. It is not
generated from Api::EndpointCatalog, so treat it as a prose surface during API
doc audits.


GET /docs/api and GET /docs/api.md

Unauthenticated public API reference. GET /docs/api renders HTML for browsers;
GET /docs/api.md returns this markdown source for agents.


GET /api/v1/version

Unauthenticated. Returns the public product identity and API documentation
version as a flat object — this endpoint does not use the standard envelope
and intentionally omits runtime and deploy internals.

Response (whole body):

{
  "app": "glnc",
  "version": "12"
}

GET /api/v1/identity

Full-account and Entity-free. Confirms the authenticated user, account, capability,
and credential type. OAuth calls also include only the current MCP connection ID
and client name. It never returns secrets, billing, configuration, or an Entity list.

Response result:

{
  "user": { "id": "...", "name": "Example User", "email": "member@example.test" },
  "account": { "id": "...", "name": "Example Company" },
  "capability": { "level": "full_account", "credential_type": "oauth" },
  "connection": { "id": "...", "client_name": "Claude" }
}

GET /api/v1/setup

Entity-scoped bootstrap. Send a bearer token plus a known X-Entity slug. Scoped
keys select their permitted entities in Settings → API Keys; full-access keys may
use any existing account entity. Returns that entity’s profile and permitted work
queues and stages. Also returns docs_url pointing to this document and
openapi_url pointing to the machine-readable contract. This endpoint does not
list entities or discover a missing slug.

Response result:

{
  "entity": {
    "slug": "my-agent",
    "entity_type": "agent",
    "emoji": "🤖",
    "max_concurrent_tasks": 1
  },
  "tag_policy": {
    "mode": "preferred",
    "preferred_tags": ["infra", "weekly"]
  },
  "work_queues": [
    {
      "slug": "GLA",
      "name": "Main Queue",
      "stages": [
        { "slug": "backlog",     "name": "Backlog",     "stage_type": "start"    },
        { "slug": "in-progress", "name": "In Progress", "stage_type": "middle"   },
        { "slug": "done",        "name": "Done",        "stage_type": "terminal" }
      ]
    }
  ],
  "hold_count": 0,
  "max_concurrent_tasks": 1,
  "docs_url": "https://glnc.io/docs/api.md",
  "openapi_url": "https://glnc.io/api/v1/openapi",
  "version": "12"
}

docs_url points to this document. openapi_url points to the OpenAPI document.
Cache them and re-fetch when version changes.

tag_policy is the account’s tag vocabulary contract — read it before assigning
work item tags or monitoring affected_systems values. Operators can change the
policy at any time without a version bump, so also re-fetch it after any
tag-policy 422. See Tag Policy for the enforcement rules.


GET /api/v1/next_work_item

Claims and returns the next available unclaimed work item for this entity. Returns null result when no item is available.

Query params:

Param Description
work_queue Slug — restrict to a single queue (optional)

Response result: full work item (see Work Item Shape) or null.


GET /api/v1/work_items

Returns work items accessible to this entity.

Query params:

Param Description
work_queue Slug — filter by queue
stage Slug — filter by stage (combine with work_queue)
mine true — only items claimed by this entity
entity Slug — items claimed by a specific entity
tag Tag label — only items carrying this tag
view all — include terminal-stage items (default: active only)

Response result: array of slim work items (no history/attachments).


GET /api/v1/work_items/:ref

Returns the full work item including history and attachments.

Response result: full work item (see Work Item Shape).


POST /api/v1/work_items

Creates a new work item.

Body (JSON or form params):

Param Required Description
work_queue Yes Queue slug
title Yes Work item title
description No Markdown description
stage No Stage slug to place item in (default: first start stage)
tags No Array of tag strings. Free-form under the preferred tag policy mode; under strict mode, newly added tags outside the account’s preferred list are rejected with 422 (see Tag Policy)
blocked_by No Array of work item refs (e.g. ["GLA-7", "GLA-12"]) that block this item. Refs must belong to the same account; unknown or cross-account refs return a 422 error and the item is not created.

Response result: full work item, including blocked (boolean) and blockers (array). See “Blocked status” below.


PATCH /api/v1/work_items/:ref

Updates a work item’s fields, or sends a heartbeat.

Update fields (JSON or form params):

Param Description
title New title
description New description (markdown)
pr_url GitHub PR URL to link
tags Array of tag strings. Free-form under the preferred tag policy mode; under strict mode, newly added tags outside the account’s preferred list are rejected with 422 (see Tag Policy)

Heartbeat (send instead of update fields):

{ "heartbeat": true }

Refreshes the claim’s last_heartbeat_at. Only the claiming entity may heartbeat. Returns { "state": "...", "last_heartbeat_at": "..." }.

When the claim is paused, the heartbeat is silently acknowledged and last_heartbeat_at is not refreshed (the reaper exempts paused claims, so there is no risk of being killed). The response still includes the current state and last_heartbeat_at so the agent can detect that it is paused. When the claim is awaiting_review, the heartbeat refreshes normally.

Response result: full work item (or heartbeat object).


POST /api/v1/work_items/:ref/transition

Moves a work item to a different stage.

Body:

Param Required Description
to Yes "next", "prev", or a stage slug
claim No "keep" (default), "release", or "checkpoint"
note No Optional markdown note to attach

Claim modes:

Value Effect
keep Stage advances; claim survives unchanged
release Stage advances; claim is destroyed; :released event recorded
checkpoint Stage advances; claim survives but is flipped to awaiting_review and frees the entity’s concurrency slot (so the agent may immediately claim other work). Means “I’ve shipped what I can — your move.” Records a :checkpointed event with from/to stages

Auto-release of awaiting_review claims. When the source claim is already awaiting_review, the next transition (by anyone permitted) destroys the claim regardless of the claim: param. Both :released and :auto_released events are recorded so reviewers can distinguish state-driven from explicit release.

Response result: full work item.

Artifact gate. If the source stage’s current_stage.artifact_spec.required is true, the engine refuses to advance unless an :artifact event of the matching kind has been recorded for the current visit (see POST /artifacts). Returns 422 with a message naming the required kind. Backward transitions and transitions into stages whose accepts_without_source_artifact is true bypass the gate.


POST /api/v1/work_items/:ref/artifacts

Records a typed artifact event for the work item’s current stage. Only the entity holding the claim may post artifacts. Multiple artifacts per visit are appended additively.

Body:

Param Required Description
kind Yes One of note, link, pr, file, json
name No Optional artifact name (e.g. plan, pull_request)
body No Free-form JSON object; defaults to {}

Response result: full work item, including the new artifact in prior_artifacts.

Errors:

  • 403 — caller is not the claim holder
  • 422 — missing or invalid kind

POST /api/v1/work_items/:ref/escalate

Escalates a work item to another entity (e.g. a human or supervisor agent).

Body:

Param Required Description
to Yes Target entity slug
message No Escalation message
decision_request No Versioned decision contract (see below) so the target gets a structured decision to answer

Response result: full work item.


POST /api/v1/work_items/:ref/decision

Records a human’s decision on an item escalated to their entity. Only the entity
the item is escalated to (or a full-access key) may decide. A terminal
disposition (approve, reject, change) records an immutable decision event,
runs the source write-back adapter, appends an immutable receipt, and moves the
item to a terminal stage; defer records the deferral and leaves the item open.
Repeating a terminal decision is a no-op.

Body:

Param Required Description
disposition Yes One of approve, reject, change, defer
option_id No The chosen option id from the decision contract
reason No Free-text reason for the decision

Response result: full work item, including its decision block and updated history.


GET /api/v1/needs

Cross-queue “needs decision” inbox: active items escalated to a human entity
across every queue on the account. Defaults to your own entity; pass
?entity=<slug> to read another. Not gated by stage permissions.

Query params:

Param Required Description
entity No Entity slug whose inbox to read (defaults to the calling entity)

Response result: array of slim work items (each with a needs_decision flag).


POST /api/v1/work_items/:ref/notes

Appends a markdown note to a work item’s history.

Body:

Param Required Description
markdown Yes Note content (markdown)

Response result: full work item.


POST /api/v1/work_items/:ref/pause

Pauses the active claim. Use this when you are blocked on something out-of-band (waiting on a deploy, a secret rotation, a design decision) and want to free your concurrency-cap slot without losing the claim. Only the claim-holding entity may pause.

A paused claim:

  • Does not count against max_concurrent_tasks — you can claim other work while paused.
  • Is not killed by the stale-claim reaper — paused claims survive indefinitely.
  • Does not refresh last_heartbeat_at on heartbeat (silent OK).

Body:

Param Required Description
reason Yes Free-text reason for pausing (max 500 chars)

Response result: full work item; claim.state will be "paused" and claim.pause_reason will echo the reason.

Errors:

  • 404 — the work item is not claimed
  • 403 — caller is not the claim holder
  • 422reason is blank or exceeds 500 chars, or the claim is already paused

POST /api/v1/work_items/:ref/resume

Resumes a paused claim. Refreshes last_heartbeat_at so the reaper does not immediately reap. Only the claim-holding entity may resume.

Body: none.

Response result: full work item; claim.state will be "active" and claim.pause_reason is cleared.

Errors:

  • 404 — the work item is not claimed
  • 403 — caller is not the claim holder
  • 422 — the claim is not currently paused

POST /api/v1/work_items/:ref/attachments

Uploads a file attachment. Use multipart/form-data.

Allowed content types: image/png, image/jpeg, image/gif, image/webp, application/pdf, application/zip, any text/*. Max size: 10 MB.

Body (multipart):

Field Required Description
file Yes The file to upload

Response result:

{
  "id": 42,
  "filename": "report.pdf",
  "url": "https://...",
  "content_type": "application/pdf",
  "byte_size": 12345
}

DELETE /api/v1/work_items/:ref/attachments/:attachment_id

Removes an attachment from a work item.

Response result: null.


POST /api/v1/monitoring/checks

Creates a monitoring check. Create-only: if a check with that name already
exists on the account, the request returns 409 and never touches the existing
row. Send Authorization: Bearer <api_key> and no X-Entity header (account-level).

Send the check name in the body (a stable, slug-like machine identifier),
alongside the same configuration fields the PUT body accepts below. To change
an existing check, use PUT /api/v1/monitoring/checks/:name instead.

Body (JSON or form params): name (required) plus every field in the PUT
body table below.

Response result: the created check config (same shape as PUT and GET).


PUT /api/v1/monitoring/checks/:name

Updates an existing monitoring check by name. Update-only: if no check
with that name exists, the request returns 404 (it never creates — use
POST /api/v1/monitoring/checks to create). This endpoint is account-level:
send Authorization: Bearer <api_key> and no X-Entity header.

PUT has replace semantics. Send the full desired config each time; configurable
fields omitted from the body reset to their defaults. Runtime fields such as
cached_status, last_run_at, last_alerted_at, and muted_until are never
changed by this endpoint.

Operator-authored remediation is protected. Once an operator edits the
remediation, remediation_steps, or remediation_inputs of a check in the
dashboard, an API call that sends an explicit value for that field returns 409
naming the conflicting fields, and persists nothing — an omitting PUT still
preserves those fields. To overwrite operator content on purpose, resend with
force_remediation_overwrite: true; force is surgical (it lifts protection only
for the fields you explicitly send, never omitted ones).

Body (JSON or form params):

Param Required Description
display_name Yes Human label for the check. Name it as the expectation itself — see Naming a monitor below.
category No Dashboard grouping label
description No Plain-English statement of exactly what this check verifies
runner No What runs/reports this check (an agent, cron, or service) — so a responder knows where to look
remediation No What to look at and how to fix it when the check fails; shown verbatim on the dashboard’s “Needs attention” panel. Also accepted as runbook (deprecated alias — remove in a follow-up).
affected_systems No Array of stored tags / blast-radius labels (what breaks if this fails), e.g. ["catalog", "weekly digest"]. Fetch GET /api/v1/monitoring/tags first to reuse existing vocabulary and avoid drift. The account tag policy applies to newly added values — under strict mode, new out-of-list values are rejected with 422; values already stored on the check are grandfathered (see Tag Policy). Returned as both affected_systems and tags for compatibility.
enabled No Boolean; defaults to true when omitted
expected_interval_seconds No Expected time between runs
warn_threshold_seconds No Warning threshold
error_threshold_seconds No Failing threshold
schedule_cron No Cron expression for schedule-based checks; mutually exclusive with expected_interval_seconds
schedule_timezone No IANA timezone required when schedule_cron is set
output_expectations No Array of { field, operator, value, severity } rules; add aggregate + window_hours for windowed trend rules (see below)
remediation_steps No Array of { text, url? } playbook steps copied into remediation work items
remediation_inputs No Object of named run-time values included in remediation work item descriptions
remediation_escalation_threshold No Failed-cycle count before escalation; default 3, use 0 to disable
remediation_work_queue No Work queue slug for remediation
remediation_stage No Stage slug within the remediation work queue

Naming a monitor: name the expectation. A monitor is an expectation, so its display_name
should be the expectation — a declarative, testable sentence about the healthy state. Follow this
pattern:

[Scope] · [Subject] [present-tense assertion] [threshold/window]
  • Scope — the service or domain, as a consistent prefix so related monitors group and sort together (GetMusic, Billing, Ingest).
  • Subject — the specific thing observed (failed posts, queue depth, daily export).
  • Assertion — a present-tense verb stating the healthy condition: clears, completes, arrives, stays below, recovers, drains.
  • Threshold/window — include it when it defines the check. “Clears” isn’t testable; “clears within 15m” is.

The litmus test: prefix the name with “We expect that…”. If it reads as a grammatical,
unambiguous sentence, the name is good. When the check fires, display_name is what renders in the
alert email subject and the Slack message — so the name alone should tell an on-call human what is
wrong, without opening the monitor.

Supporting rules:

  • One expectation per monitor. If you can’t name it in one clause, it’s probably two monitors.
  • Always phrase the healthy state, never the failureQueue depth stays below 10k, not Queue backed up. This keeps every name consistent with the “expectation violated” framing.
  • Use a consistent delimiter between scope and assertion (·, |, or ) so a list of monitors scans cleanly.
  • Concrete nouns over internal shorthand — names should survive team turnover.
  • Numbers only when they define the expectation — windows and thresholds yes, incidental config values no.
  • Keep it under ~60 characters. Alert email subjects truncate the name past that, which would cut off the very clause that makes it testable.

Good:

GetMusic · Failed posts clear from buffer within 15m
Billing · Invoices generate by 02:00 UTC daily
Ingest · Partner feed arrives every 6h
Search · p95 latency stays below 400ms

Avoid: GetMusic Buffer failed posts clear — parses ambiguously, buries the subject, and has no
window, so there is no testable expectation in it.

How this relates to the other fields. The scope prefix does not replace category — keep setting
category for dashboard grouping. The prefix earns its place because an alert arrives without the
dashboard around it, so the name must carry its own context. And display_name does not replace
description: the name is the short, scannable expectation; description is the fuller what-and-why.

Note the distinction between the two name fields: the :name in the URL is a stable machine
identifier
(a short slug, lowercase, immutable once created) — keep it short and slug-like. The
naming guidance above is about display_name, the human-facing label.

None of this is enforced. No name is validated, rejected, or flagged for not following the pattern —
it is a recommendation. Following it has a useful side effect: a well-named monitor list doubles as
documentation, because reading it top to bottom is a list of everything the system promises to do.

Triage context (description, runner, remediation, affected_systems). These are optional but
make a failure actionable: when a check becomes Failing, the dashboard’s “Needs attention” panel
shows its reason alongside what runs it, its tags/blast-radius labels, and the remediation note (verbatim). When
declaring checks as code, set them on the same PUT — and because PUT replaces,
include them on every sync or they reset to empty.

Output expectation rules. Two kinds:

  • Latest-run rule (default): { "field": "records_imported", "operator": ">=", "value": 50, "severity": "critical" } — checks the most recent run’s metadata.
  • Windowed rule: add aggregate (avg/sum/min/max/count) and window_hours (1–2160) to assert a trend across all runs in a trailing window. { "field": "sales_count", "operator": ">=", "value": 7, "aggregate": "avg", "window_hours": 168, "severity": "critical" } catches “sales quietly down all week” even when every run succeeds. count counts runs in the window (field optional): { "operator": ">=", "value": 1, "aggregate": "count", "window_hours": 24, "severity": "warning" } means “expect at least one run per day”.

Severity warning turns the check yellow when the rule fails; critical turns it red.

Response result: monitoring check config, including status, status_reason,
tags, affected_systems, remediation_attempt_count, next_expected_run,
acknowledged, acknowledged_at, acknowledgment_note, acknowledgment_expires_at,
and acknowledgment_expires_on_next_run.


GET /api/v1/monitoring/checks

Lists every monitoring check on the account, each in the same shape as
GET /api/v1/monitoring/checks/:name. Use this to reconcile declared-as-code
config against what’s live (find strays to delete, verify a deploy synced
everything) or to triage failures in one call.

Response result: { "count": N, "checks": [ ...check configs... ] }


GET /api/v1/monitoring/checks/:name

Returns a monitoring check’s current configuration and status fields. Unknown and
cross-account check names return the same 422 not-found response.

Response result: same check config shape as PUT.


DELETE /api/v1/monitoring/checks/:name

Permanently deletes a monitoring check and its dependent runs, acknowledgments,
and status history. Use this to retire a check entirely. To keep history but
pause evaluation, update the check with enabled: false instead.

Unknown and cross-account check names return the same 422 not-found response.

Response result: { "name": "check_name" }


POST /api/v1/monitoring/checks/:name/acknowledge

Acknowledges the current Failing/Warning incident for a check (red/yellow
status token). Acknowledgment does not change the status token: the check still
appears in dashboards and daily digests, and immediate alert emails + webhooks
are suppressed. The acknowledgment clears when the check returns to Passing
(green status token) — or expires after ttl_hours of continued failure
(PagerDuty-style ack-timeout), at which point a still-failing check re-alerts.
Acks therefore cannot be set-and-forgotten.

Body:

Param Required Description
note No Optional operator note
ttl_hours No Re-alert window in hours, 1–168 (clamped), or "next_run" to clear on the next terminal run with a 48h safety net. Default 48.

Repeated calls are idempotent and refresh the note. Green, unknown, muted, and
disabled checks return 422 because there is no red/yellow incident to acknowledge.

Response result: monitoring check config with acknowledged: true.


DELETE /api/v1/monitoring/checks/:name/acknowledge

Clears a monitoring acknowledgment. This is idempotent: clearing a check that is
not acknowledged returns 200.

Response result: monitoring check config with acknowledged: false.


GET /api/v1/monitoring/status

Returns the account’s cached monitoring health summary. overall is green
when every check is green or acknowledged, awaiting when checks are newly
created and not yet overdue, and unhealthy unacknowledged checks appear in
attention. Known-but-acknowledged incidents appear separately.

Response result: { "overall": "green", "counts": { ... }, "attention": [ ...check summaries... ], "acknowledged": [ ...check summaries... ] }


GET /api/v1/monitoring/tags

Lists all distinct tags in use across the account’s checks, sorted alphabetically with
per-tag check counts.

Call this before creating or updating checks to reuse existing vocabulary and avoid
near-duplicate drift (e.g. “GetMusic” vs “get-music” vs “GetMusic catalog”). Account-level
— full-access API key, no X-Entity header.

Response result: { "count": N, "tags": [{ "name": "...", "count": N }, ...] }


POST /api/v1/monitoring/tags/delete

Removes a tag from every check that carries it. A tag matching no checks is a clean
no-op (affected: 0). Account-level — full-access API key, no X-Entity header.

Body:

Param Required Description
name Yes Tag name to delete

Response result: { "affected": N } — count of checks that were updated.


POST /api/v1/monitoring/tags/rename

Renames a tag across all checks. If the target name already exists on some checks,
the source is merged cleanly — no duplicate entries. Account-level — full-access API key,
no X-Entity header.

Honors the account tag policy: under strict mode, renaming to a to tag outside
the preferred list returns 422 unless every affected check already carries it
(see Tag Policy).

Body:

Param Required Description
from Yes Current tag name
to Yes New tag name

Response result: { "affected": N } — count of checks that were updated.


POST /api/v1/monitoring/tags/merge

Collapses one or more source tags into a single target tag across all checks. Checks
that carry multiple source tags end up with a single target entry. Account-level —
full-access API key, no X-Entity header.

Honors the account tag policy: under strict mode, merging into an into tag outside
the preferred list returns 422 unless every affected check already carries it
(see Tag Policy). POST /api/v1/monitoring/tags/delete is unaffected.

Body:

Param Required Description
sources Yes Array of tag names to collapse
into Yes Target tag name

Response result: { "affected": N } — count of checks that were updated.


POST /api/v1/monitoring/checks/:check_name/runs

Reports one execution signal for a monitoring check. Send status=started to open
a scheduled run, or send succeeded, failed, or timed_out to record a terminal
run in one call. This endpoint is account-level:
send Authorization: Bearer <api_key> and no X-Entity header.

Body (JSON or form params):

Param Required Description
status No started, succeeded, failed, or timed_out; defaults to succeeded
metadata No JSON object stored as run payload
summary No Markdown narrative for the run, capped at 64 KB, stored on the run and rendered sanitized on the run detail page
started_at No ISO 8601 timestamp
finished_at No ISO 8601 timestamp
error_message No Failure details
source No Caller-supplied reporting channel label; defaults to api
host No Optional self-reported machine/host identifier for this run (e.g. office-ap). Stored and shown as self-reported — not verified. The reporting API key is recorded server-side as the authoritative origin regardless.

Example with a human-readable summary:

{
  "status": "succeeded",
  "metadata": { "records_processed": 42 },
  "summary": "Checked the nightly scrape.\n\n| Metric | Value |\n| --- | ---: |\n| Records | 42 |\n\nNo retries were needed."
}

Summaries are Markdown-only input: glnc renders them server-side and sanitizes the
HTML before showing it on the run detail page. Avoid putting secrets, tokens, or
private URLs in summaries; they are free-form text visible to users in the account.

Response result:

{ "run_id": 123, "check": "nightly-scrape", "status": "started", "expected_slot": "2026-07-09T04:00:00Z", "summary": "Checked the nightly scrape." }

POST /api/v1/monitoring/checks/:check_name/runs/complete

Completes the only open run for a monitoring check. If no run is open, glnc records
a standalone terminal run. If more than one run is open, complete one explicitly
with the member route below. Account-level — full-access API key, no X-Entity
header.

Body (JSON or form params):

Param Required Description
status No succeeded, failed, or timed_out; defaults to succeeded
metadata No JSON object stored as run payload
summary No Markdown narrative for the run, capped at 64 KB
finished_at No ISO 8601 timestamp
error_message No Failure details
source No Caller-supplied source label; defaults to api

POST /api/v1/monitoring/checks/:check_name/runs/:id/complete

Completes a specific open monitoring run. Already-terminal runs are returned
unchanged, so callers can safely retry this request after a network failure.
Account-level — full-access API key, no X-Entity header.

Body (JSON or form params):

Param Required Description
status No succeeded, failed, or timed_out; defaults to succeeded
metadata No JSON object stored as run payload
summary No Markdown narrative for the run, capped at 64 KB
finished_at No ISO 8601 timestamp
error_message No Failure details
source No Caller-supplied source label; defaults to api

GET /api/v1/surveys/active

Returns the single best active survey for the caller’s supplied user/context, or
survey: null when nothing should be shown. This endpoint is account-level:
send a full-access Authorization: Bearer <api_key> and no X-Entity header.

Query params:

Param Description
user_ref Optional opaque user identifier used for frequency-cap tracking
context Optional hash evaluated by targeting rules, such as context[page], context[trigger], or context[device]

Response result:

{
  "survey": {
    "id": 42,
    "title": "Checkout feedback",
    "question_text": "What almost stopped you from checking out?",
    "rating_enabled": true,
    "tags": ["checkout"],
    "status": "active",
    "priority": 10,
    "created_at": "2026-07-09T12:00:00Z"
  }
}

POST /api/v1/surveys/:survey_id/responses

Records a response to an active survey in the API user’s account. Missing,
inactive, and cross-account surveys return 404. This endpoint is account-level:
full-access bearer token, no X-Entity header.

Body:

Param Required Description
answer_text Yes Free-text answer
rating No Optional numeric rating
user_ref No Same opaque user identifier used by survey delivery
context No Optional hash saved with the response

Response result:

{ "response_id": 123 }

GET /api/v1/responses

Returns the cross-survey response queue for planning agents. This endpoint is
account-level: full-access bearer token, no X-Entity header.

Query params:

Param Description
status new, acknowledged, or archived; defaults to new

Returns newest-first, up to 50 responses.

Response result:

{
  "status": "new",
  "count": 1,
  "responses": [
    {
      "id": 123,
      "survey_id": 42,
      "answer_text": "I could not find pricing.",
      "rating": 3,
      "user_ref": "user-abc",
      "context": { "page": "pricing" },
      "status": "new",
      "acknowledged_at": null,
      "acknowledged_by": null,
      "disposition": null,
      "archived_at": null,
      "created_at": "2026-07-09T12:00:00Z"
    }
  ]
}

POST /api/v1/responses/:id/acknowledge

Marks a survey response acknowledged with provenance. Idempotent. This endpoint
is account-level: full-access bearer token, no X-Entity header.

Body:

Param Required Description
disposition No Optional note explaining how the response was handled
run_id No Optional agent run id; provenance is agent:<run_id> when present, otherwise agent:api_user:<id>

Response result: { "response": { ...response shape... } }


POST /api/v1/responses/:id/archive

Archives a survey response. Idempotent. This endpoint is account-level:
full-access bearer token, no X-Entity header.

Body:

Param Required Description
run_id No Optional agent run id; provenance is agent:<run_id> when present, otherwise agent:api_user:<id>

Response result: { "response": { ...response shape... } }


GET /api/v1/insights

Returns the cross-survey synthesis digest for planning agents. This endpoint is
account-level: full-access bearer token, no X-Entity header.

Query params:

Param Description
status new, acknowledged, or archived; new maps to pending insights
since Optional ISO 8601 timestamp applied to last_response_at

Response result:

{
  "synthesis": {
    "enabled": true,
    "min_responses": 25,
    "total_responses": 20
  },
  "insights": [
    {
      "id": 77,
      "theme": "pricing confusion",
      "summary": "Users could not find plan differences.",
      "quotes": ["I could not find pricing."],
      "response_count": 8,
      "last_response_at": "2026-07-09T12:00:00Z",
      "status": "pending"
    }
  ],
  "verbatims": []
}

GET /api/v1/surveys/:survey_id/insights

Returns the same synthesis envelope as GET /api/v1/insights, scoped to one
survey. When synthesis has not run yet, the response includes recent raw
verbatims for that survey. This endpoint is account-level: full-access bearer
token, no X-Entity header.

Query params: same as GET /api/v1/insights.


POST /api/v1/insights/:id/acknowledge

Acknowledges a survey insight and cascades the disposition to its constituent
responses. Idempotent. This endpoint is account-level: full-access bearer token,
no X-Entity header.

Body:

Param Required Description
disposition No Optional disposition. Defaults to synthesized:<theme>
run_id No Optional agent run id; provenance is agent:<run_id> when present, otherwise api_user:<id>

Response result: { "insight": { ...insight shape... } }


Work Item Shape

Full (returned by show, create, update, transition, escalate, note)

{
  "ref": "GLA-42",
  "title": "Fix login bug",
  "description": "Users cannot log in when ...",
  "tags": ["auth", "login"],
  "position": 3,
  "work_queue": { "slug": "GLA", "name": "Main Queue" },
  "current_stage": {
    "slug": "in-progress",
    "name": "In Progress",
    "stage_type": "middle",
    "instructions": "Implement the fix and open a PR.",
    "artifact_spec": { "required": true, "kind": "pr", "name": "pull_request" },
    "accepts_without_source_artifact": false
  },
  "claim": {
    "entity_slug": "my-agent",
    "emoji": "🤖",
    "claimed_at": "2025-01-01T10:00:00Z",
    "last_heartbeat_at": "2025-01-01T10:05:00Z",
    "state": "active",
    "pause_reason": null
  },
  "pr": {
    "number": 123,
    "url": "https://github.com/org/repo/pull/123",
    "status": "open",
    "merged_at": null
  },
  "attachments": [
    {
      "id": 1,
      "filename": "screenshot.png",
      "url": "https://...",
      "content_type": "image/png",
      "byte_size": 48320
    }
  ],
  "blocked": true,
  "blockers": [
    {
      "ref": "GLA-7",
      "title": "Add OAuth handler",
      "stage_slug": "review",
      "stage_type": "middle",
      "blocking": true
    }
  ],
  "prior_artifacts": [
    {
      "stage": "plan",
      "kind": "note",
      "name": "plan",
      "body": { "summary": "Approach: refactor the auth check first." },
      "produced_at": "2026-04-26T09:30:00Z",
      "produced_by": "my-agent"
    }
  ],
  "history": [
    { "type": "created",      "actor": "my-agent", "at": "2025-01-01T09:00:00Z" },
    { "type": "stage_change", "actor": "my-agent", "at": "2025-01-01T10:00:00Z", "from": "backlog", "to": "in-progress" },
    { "type": "artifact",     "actor": "my-agent", "at": "2026-04-26T09:30:00Z", "stage_slug": "plan", "kind": "note", "name": "plan", "body": {} }
  ]
}

History entries always have type, actor, and at. Any additional event-specific data is spread inline at the top level (not nested under data).

Event Types

type When emitted
created Work item created
stage_change Item moved to a different stage (data: from, to)
note Markdown note appended (data: markdown)
claimed Entity claimed the item
released Claim released
escalated Escalated to another entity (data: to, message)
pr_opened GitHub PR linked (data: pr_number, url)
pr_updated PR status changed (data: pr_number, url)
pr_merged PR merged (data: pr_number, url)
attachment_added File attached (data: filename, content_type)
attachment_removed File removed (data: filename, content_type)
artifact Stage artifact recorded (data: stage_slug, kind, name, body)
paused Claim was paused (data: reason)
resumed Claim was resumed
checkpointed Claim was flipped to awaiting_review via transition (data: from, to)
auto_released Claim was destroyed by a transition while in awaiting_review (data: from_state)

Slim (returned by index)

Same as full but without instructions, attachments, history, prior_artifacts, current_stage.artifact_spec, current_stage.accepts_without_source_artifact, and claim.emoji. Always includes blocked and blockers.

Blocked status

blocked is true when at least one entry in blockers has blocking: true (the blocker’s stage is non-terminal). When the last open blocker reaches a terminal stage, blocked flips to false and the item becomes claimable again.

GET /api/v1/next_work_item skips items where blocked: true — agents will not be handed actively-blocked work. The board UI displays a red ✕ on blocked cards with a hover tooltip listing each open blocker.

Each blocker row:

Field Description
ref Blocker’s friendly ref (e.g. GLA-7)
title Blocker’s title
stage_slug Blocker’s current stage slug
stage_type start, middle, or terminal
blocking true if stage_type != "terminal" (i.e. this entry is what’s holding the item back)

Stage Types

Type Meaning
start Items enter the queue here
middle Work in progress — any non-start, non-terminal stage
terminal Work is complete or closed

Typical Agent Loop

1. GET /api/v1/setup          — learn queue topology
2. GET /api/v1/next_work_item — claim a task
   → if null, sleep and retry
3. Read current_stage.instructions
4. Do the work
5. PATCH /:ref                — update description / pr_url as you go
   PATCH /:ref heartbeat:true — every ~60s to keep claim alive
6. POST /:ref/transition to:"next"  — advance when done
7. Go to step 2

If you get stuck, use POST /:ref/escalate to hand off to a human or supervisor agent.

Handling blocks mid-stage

When you are blocked on something out-of-band but expect to come back to the same work:

  • POST /:ref/pause { "reason": "..." } frees your cap slot but keeps the claim. Your heartbeat goes silent (no refresh) but the reaper will not kill the claim. Resume with POST /:ref/resume when unblocked.
  • POST /:ref/transition { "to": "...", "claim": "checkpoint" } advances the stage and flips your claim to awaiting_review. Use this when you have shipped what you can and need a human (or another entity) to take the next step. The parked claim does not count against your concurrency cap, so you may immediately call GET /api/v1/next_work_item to pick up the next item. The next transition by anyone destroys the parked claim automatically.

Monitoring Remediation Loop

When a check becomes Failing, glnc automatically opens a work item in the check’s configured
remediation_work_queue / remediation_stage. A self-healing agent follows these steps to claim
the item, apply the fix, and confirm the check is healthy again.

1. GET /api/v1/next_work_item              — claim the remediation work item
   → title: "Check failing: <display_name>"; description carries the check name,
     failure reason, last-run time, and a link to the monitoring dashboard
2. GET /api/v1/monitoring/checks/:name     — read the check's remediation field
   → result.remediation is the operator-authored fix note
     (full-access API key — no X-Entity header)
3. Apply the fix
4. POST /api/v1/monitoring/checks/:check_name/runs   { "status": "succeeded" }
   → the check becomes Passing immediately; the incident is resolved
5. POST /api/v1/work_items/:ref/transition     { "to": "next", "claim": "release" }
   → the work item does NOT close when the check becomes Passing; advance it manually

Fix guidance vs. work item description

The check’s remediation field (step 2) is the operator-authored procedure: what to look at and
how to fix it. The work item description (auto-generated by glnc when the item is opened) carries
runtime context — failure reason, last-run time, dashboard link — not the fix procedure. Read
both: the work item description tells you what broke, the remediation field tells you how to
fix it.

Account-level endpoints in this loop

GET /api/v1/monitoring/checks/:name and POST /api/v1/monitoring/checks/:check_name/runs are
account-level: send a full-access Authorization: Bearer <api_key> and no
X-Entity header (same rule as all other monitoring endpoints).

Idempotency

While an open remediation item exists for a check, subsequent red states reuse it — glnc does not
create duplicates. A fresh item opens only after the previous one reaches a terminal stage or is
deleted. Muted and disabled checks are skipped.

Glnc uses essential storage for the app. Optional analytics stay off unless you allow them.