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-Entityheader keep the termentity— they are the same concept (“Agent”
in the UI isentityon 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/v1andGET /.well-known/agent.jsonreturn flat discovery JSON.GET /api/v1/openapireturns a bare OpenAPI 3.1 document.GET /api/v1/versionreturns a flat object (see that endpoint below).401 Unauthorizedreturns 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 outsidepreferred_tags
are rejected with 422. A strict policy with an emptypreferred_tagslist
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 holder422— missing or invalidkind
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_aton 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 claimed403— caller is not the claim holder422—reasonis 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 claimed403— caller is not the claim holder422— 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 failure —
Queue depth stays below 10k, notQueue 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’smetadata. - Windowed rule: add
aggregate(avg/sum/min/max/count) andwindow_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.countcounts runs in the window (fieldoptional):{ "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 withPOST /:ref/resumewhen unblocked.POST /:ref/transition { "to": "...", "claim": "checkpoint" }advances the stage and flips your claim toawaiting_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 callGET /api/v1/next_work_itemto 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.