Documentation
API
REST API for external systems to create and manage Syrus Jobs.
REST API
Syrus exposes two REST surfaces. The user API under /api/v1/app/* is the programmatic version of the browser app: personal API tokens authenticate as their owning user and inherit that user's repository, Job, Epic, chat, and settings permissions. The operator API under /api/v1/admin/* is for instance-wide automation and requires an admin user's token.
curl -H "Authorization: Bearer $SYRUS_API_TOKEN" \
https://syrus.example.com/api/v1/admin/overview
Non-admin tokens are accepted on /api/v1/app/* endpoints and are scoped by the same authorization policies as the browser UI. /api/v1/admin/* endpoints reject non-admin tokens with 403 Forbidden; missing or invalid tokens return a JSON 401 error.
The generated user API catalog lists every registered /api/v1/app/* endpoint. Operator/admin endpoint inventory lives with the operator docs in config/syrus_docs/admin_api_catalog.md.
Repeated invalid bearer tokens from the same source IP are rate-limited (20 per 5 minutes); once tripped, further bad-token attempts from that IP get a 429 Too Many Requests with error.code rate_limited until the window expires. A request with no Authorization header, or a valid token, never counts against or is blocked by this limit.
Command Line Client
The repository includes a standalone Go CLI under cli/. It sends the same bearer token header as the REST examples and exposes chat, inbox, checkout, Job, Epic, repository, and schedule commands. See Syrus CLI for installation, login, command reference, and terminal workflows.
Browser Sessions
Browser clients can end the current signed-cookie session with DELETE /api/v1/app/auth/session. The endpoint returns a JSON redirect path for the sign-in screen and clears the browser session cookie. Bearer tokens are not browser sessions, so bearer-token requests receive token_session_not_destroyable instead of silently succeeding.
Terminal Sessions
When the terminal plugin is enabled, user-scoped app API clients can manage their own interactive terminal sessions. GET /api/v1/app/terminal_sessions lists the authenticated user's running sessions, and GET /api/v1/app/terminal_sessions/open_count returns just the count. With the plugin disabled these endpoints return plugin_disabled. Admins can use POST /api/v1/app/terminal_sessions to create a session with an optional workflow_id, candidate_key, and name. Syrus selects the working directory from the chosen workspace; scratch sessions use the app root, and caller-supplied filesystem paths are ignored.
curl -X POST https://syrus.example.com/api/v1/app/terminal_sessions \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "workflow_id": 8454, "name": "Workflow 8454" }'
GET /api/v1/app/terminal_sessions/:id returns one session scoped to the authenticated user, and POST /api/v1/app/terminal_sessions/:id/kill marks that session killed so the relay terminates the PTY. Session payloads include id, name, working_directory, relay_address, timestamps, outcome, and workflow_id; they never include the relay auth token.
Admin API clients get the same surface across every user: GET /api/v1/admin/terminal_sessions lists sessions, filterable by ?state=running|finished, ?user=<email substring>, and ?hostname=<relay host>; GET /api/v1/admin/terminal_sessions/:id returns one session; POST /api/v1/admin/terminal_sessions/:id/kill kills it through the same write path the app API uses. Admin payloads add state, hostname, age_s, and an owning user summary. With the plugin disabled, every endpoint — app and admin — returns plugin_disabled.
curl -H "Authorization: Bearer $SYRUS_API_TOKEN" \
"https://syrus.example.com/api/v1/admin/terminal_sessions?state=running"
Throughput Metrics
Authenticated app clients can read the versioned repository snapshot at GET /api/v1/app/repositories/:id/throughput_metrics. The payload's cache_status is fresh, computed, or stale; generated_at remains the time its metric windows were computed. If a cold snapshot is already being computed elsewhere, the endpoint returns 503 with throughput_snapshot_unavailable rather than an empty metric set.
When the throughput plugin is enabled, admin API clients can pull instance-wide throughput without writing SQL against production:
curl -H "Authorization: Bearer $SYRUS_API_TOKEN" \
"https://syrus.example.com/api/v1/admin/throughput?since=2026-08-30T00:00:00Z&until=2026-09-06T00:00:00Z"
GET /api/v1/admin/throughput returns Jobs created versus closed per bucket, Jobs reaching implemented per bucket, and the median/p90 cycle time from Job creation to pr_merged, bucketed by both hour and day so the same call covers incident work and trend watching. Pass repository (a numeric id or owner/name slug) to scope to one repository instead of the whole instance, and since/until (ISO-8601, consistent with /api/v1/admin/runs?since=) to bound the window -- default is the trailing 7 days, clamped to at most 90. All timestamps in the response are UTC. With the plugin disabled this endpoint returns plugin_disabled.
Create a Direct Job
POST /api/v1/admin/jobs creates a direct Job and starts the normal initial workflow when the Job is not blocked by dependencies or an Epic. The Job belongs to the owner of the target repository, so the repository owner's GitHub and agent credentials are used.
curl -X POST https://syrus.example.com/api/v1/admin/jobs \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"job": {
"repository": "acme/widgets",
"title": "Update the README",
"prompt": "Add API setup instructions to the README.",
"priority": "high",
"agent_provider": "codex"
}
}'
Use either repository_id or repository/repo as owner/name. Optional fields are title, priority (high, medium, low), agent_provider, epic_id, and owner_user_id. When title is blank or omitted, Syrus derives a short deterministic title from the prompt.
POST /api/v1/app/jobs is the non-admin equivalent — the Job belongs to the authenticated token user rather than the repository owner, and it accepts either repository_id or repository/repo as an owner/name slug for one of that user's own active repositories (the syrus job create CLI command posts here). It accepts the same optional title, prompt, priority (urgent, high, medium, low), agent_provider, epic_id, and owner_user_id fields; epic_id must reference an Epic in the same repository, and owner_user_id must reference a user who is a member of that repository.
Invoke an Operator Action
POST /api/v1/admin/pending_actions/invoke runs one registered PendingActions operation by action_key. This is the scriptable equivalent of confirming a chat pending action: there is no confirmation token or pending state, and the API call itself is the confirmation. The endpoint requires an admin API token and a non-blank reason; successful calls record an audit row with the acting user, action key, payload, reason, and result.
curl -X POST https://syrus.example.com/api/v1/admin/pending_actions/invoke \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action_key": "reenqueue_work",
"reason": "Queued run was left behind after worker restart.",
"payload": { "job_id": 42, "run_id": 314 }
}'
If the operation's own payload validation rejects the request, the response is 422 with error.code invalid_pending_action_payload. If an operation raises ArgumentError for a user-facing condition, such as a missing Run or an ineligible state, the response is also 422 with error.code pending_action_operation_failed. Chat-bound operations that require a live chat session or a persisted chat pending-action record, such as Coding Mode handoffs, are rejected before execution rather than being auto-confirmed through this endpoint.
Submit Job Feedback
All job and epic endpoints accept a numeric ID, the JOB-<n> / EPIC-<n> prefix format, or a human-readable slug derived from the issue or epic title (e.g. /api/v1/app/jobs/repair-aqueduct). Slugs are auto-generated at Job / Epic creation and are unique per instance.
GET /api/v1/app/slug_refs/:slug resolves canonical display refs such as JOB-<id>, EPIC-<number>, CHAT-<id>, and WF-<id> through the shared slug registry. The response includes the canonical ref, type, prefix, numeric id, access flag, app path, preview API path when available, copy/linkification defaults, and mobile interaction hints. Known-but-missing or inaccessible refs return a neutral payload with accessible: false and no app path; malformed known refs return 400, and unknown prefixes return 404. /api/v1/app/slugs/:slug is an alias, and browser navigation can use either /s/:slug or a bare root ref such as /JOB-<id> to redirect accessible refs to their app page.
GET /api/v1/app/workflows/:workflow_id/preview returns the focused workflow preview used by slug hover and touch interactions: canonical slug and path, human-readable workflow type, workflow state, and a compact projected step list. Access follows the parent Job's visibility rules.
POST /api/v1/app/jobs/:id/chat_feedback lets the authenticated job owner submit follow-up feedback directly, without confirming a chat pending action. The Job must belong to the token user and be in implemented or failed state. Syrus creates a chat_feedback Workflow on the existing branch.
curl -X POST https://syrus.example.com/api/v1/app/jobs/123/chat_feedback \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "body": "Please simplify the settings copy and keep the existing layout." }'
Blank feedback or a non-actionable Job returns 422 with a JSON error.
Target Graph Inspection
User-scoped app API clients can inspect compiled project and target graph data for repository and workflow debugging:
GET /api/v1/app/repositories/:id/target_graphGET /api/v1/app/jobs/:job_id/target_graphGET /api/v1/app/workflows/:workflow_id/target_graph
Read access follows the same repository and Job visibility rules as the app UI. The repository endpoint compiles the default branch. The Job endpoint uses the latest Workflow unless workflow_id is provided, and the Workflow endpoint inspects that concrete Workflow.
The endpoints accept limit, offset, project_id, kind, and q. limit defaults to 500 and is capped at 2,000. They also accept mode=neighborhood for progressive graph rendering. Neighborhood requests can send focus_label, focus_state (failing, selected, skipped, or cached), direction (both, dependencies, or dependents), and depth (capped at 4). selected, skipped, and cached come from workflow artifacts, so use the Job or Workflow endpoint for those runtime states; repository graphs support label, project/filter, and failing-health entry points. Responses include projects, windowed or neighborhood targets, dependency edges, compiler diagnostics, and page-scoped target health:
curl "https://syrus.example.com/api/v1/app/jobs/123/target_graph?limit=100&kind=grader" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
curl "https://syrus.example.com/api/v1/app/jobs/123/target_graph?mode=neighborhood&focus_state=selected&depth=2" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
Each target includes its label, kind, project id, source scope, dependencies, owning config path, executable flag, and executable metadata such as command, phases, requiredness, timeout, and raw target metadata. Workflow-scoped responses also include selection overlays when the Workflow recorded them: selected, skipped, or cached, with the recorded reason and any available target-health references.
health.targets and health.summary are scoped to the current target window, not the whole repository graph. Page through the target list to inspect more health rows without asking the server to serialize an unbounded graph.
Diff Review Comments
User-scoped app API clients can persist Syrus-owned diff review comments without coupling the review UI to GitHub PR comments. GET /api/v1/app/jobs/:job_id/diff_review_comments lists comments visible for a Job. It accepts optional surface, base_ref, head_ref, path, and state, workflow_id, and run_id filters. Read access follows normal Job visibility. Source-browser diff comments use surface=job_source_diff; run artifact diffs use surface=run_agent_diff or surface=run_step_agent_diff, and should include workflow_id plus run_id when listing comments for a concrete artifact panel.
POST /api/v1/app/jobs/:job_id/diff_review_comments creates a comment, and PATCH /api/v1/app/jobs/:job_id/diff_review_comments/:id updates one. POST /api/v1/app/jobs/:job_id/diff_review_comments/:id/resolve marks a comment resolved. Mutations use the same Job write policy as approve, retry, and feedback actions: the job owner, an admin, or a write-tier repository member.
DELETE /api/v1/app/jobs/:job_id/diff_review_comments/:id hard-deletes a comment that is still in draft — once a comment has been submitted, resolved, or superseded it's part of the review history and the endpoint returns 422 instead. On success the response is { "job_id": ..., "deleted_id": ... }.
POST /api/v1/app/jobs/:job_id/diff_review_comments/submit accepts comment_ids and starts a chat_feedback workflow for the selected unresolved comments. The workflow receives both a readable feedback body and structured diff_comments artifacts containing the comment body, path, side, old/new line coordinates, base/head refs, hunk snapshot, optional Workflow/Run links, and context. Accepted comments are marked submitted and linked to the created workflow; duplicate active chat_feedback submissions for the same Job return a validation error.
curl -X POST https://syrus.example.com/api/v1/app/jobs/123/diff_review_comments \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"diff_review_comment": {
"surface": "job_source_diff",
"base_ref": "base-sha",
"head_ref": "head-sha",
"path": "app/models/widget.rb",
"side": "right",
"new_line": 42,
"diff_hunk": "@@ -40,2 +40,3 @@",
"body": "Please add a regression spec.",
"context": { "symbol": "Widget#call" }
}
}'
Comment payloads include durable anchors (path, side, old_line, new_line, base/head refs, and hunk/context data), lifecycle state (draft, submitted, resolved, or superseded), timestamps, and author details. Responses include both a flat comments array and by_path, keyed by file path and an anchor key such as right::42 or left:38:.
Every comment also carries anchor_kind, either "line" (the default, tied to a specific file/side/line) or "review" for a whole-review comment that isn't anchored to any code line. Create a whole-review comment by posting anchor_kind: "review" with no path/side/old_line/new_line — Syrus clears those fields regardless of what's sent. Whole-review comments submit through the same submit endpoint alongside line-anchored ones and appear in by_path under the "__review__" group with anchor key "review".
File a Syrus Report Issue
POST /api/v1/app/report_issue lets the authenticated user file a GitHub issue against the configured Syrus report repository. The default target is tkadauke/syrus; operators can change AppSetting.report_issue_repo_slug. The user must have a connected GitHub token.
curl -X POST https://syrus.example.com/api/v1/app/report_issue \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "title": "Chat composer bug", "body": "Context and reproduction steps." }'
The response is { "issue_url": "https://github.com/..." }. Missing title or GitHub credentials returns 422 with a JSON error.
Create an Epic
POST /api/v1/admin/epics creates an Epic in a repository owned by the repository's Syrus user.
curl -X POST https://syrus.example.com/api/v1/admin/epics \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"epic": {
"repository": "acme/widgets",
"title": "Documentation cleanup",
"description": "Group the docs polish work.",
"auto_approve_mode": "never"
}
}'
Optional fields are github_issue_url, owner_user_id, and auto_approve_mode.
User-scoped clients can also create Epics through the app API with any user API token when the caller has write-tier repository access (directly, through a team grant, or as a global admin). Use repository/repo as an owner/name slug, or repository_id when the caller already has the database id:
curl -X POST https://syrus.example.com/api/v1/app/epics \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"epic": {
"repository": "acme/widgets",
"title": "Documentation cleanup",
"description": "Group the docs polish work."
}
}'
Manage Design Docs
When the design_docs plugin is enabled, admin tokens can manage design docs through /api/v1/admin/design_docs instead of the SPA's session-shaped app API. GET /api/v1/admin/design_docs lists docs across every owner, filterable by state (draft/accepted/archived), visibility (private/public), and user (a substring match against the owner's email address).
curl -X POST https://syrus.example.com/api/v1/admin/design_docs \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"design_doc": {
"title": "Deploy runbook",
"markdown": "# Deploy runbook\n\n..."
}
}'
GET /api/v1/admin/design_docs/:id returns the full detail payload including current Markdown. PATCH /api/v1/admin/design_docs/:id updates title/markdown/state and, on a canonical edit, records a new version — every admin PATCH checkpoints by default, so it always produces a version row the way an explicit Save does in the editor. Archived docs are non-destructive records; owners can restore them to draft from the Design Docs editor. GET /api/v1/admin/design_docs/:id/versions returns the version history. With the plugin disabled, every endpoint answers plugin_disabled.
Scheduled Tasks
When the scheduled_tasks plugin is enabled (on by default), admin tokens can inspect and operate ScheduledTasks instance-wide — the same pause/unpause/fire actions available from the Schedules UI, reachable without a browser session. With the plugin disabled these endpoints return plugin_disabled. Authoring (create/edit/delete) stays app-API/SPA-only.
GET /api/v1/admin/scheduled_tasks lists tasks across every repository and user, filterable by repository (owner/name), user (email substring), kind (cron or one_shot), paused (true/false), and due_before (ISO 8601 — tasks whose next fire time falls before it). Archived tasks are excluded by default.
curl "https://syrus.example.com/api/v1/admin/scheduled_tasks?paused=true" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
GET /api/v1/admin/scheduled_tasks/:id returns full detail, including the cron expression, pr_pileup_policy, consecutive_failure_count, last_fired_at, and next_fire_at.
POST /api/v1/admin/scheduled_tasks/:id/pause and .../unpause toggle a task the same way the operator UI does — useful for silencing a task that is filing broken Jobs every hour without waiting on AppSetting.max_job_failures to auto-pause it.
POST /api/v1/admin/scheduled_tasks/:id/fire fires the task once immediately, producing the same kind=cron Job the poller would (prompt rendered at fire time), without waiting for its next scheduled window. Useful for testing a task's prompt end-to-end.
curl -X POST https://syrus.example.com/api/v1/admin/scheduled_tasks/42/fire \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
GET /api/v1/admin/cron_templates and GET /api/v1/admin/cron_templates/:id are read-only lookups for the reusable schedule + prompt templates tasks can be created from, filterable by user.
Test Insights
Requires the test_insights plugin (bundled, on by default). It exposes an operator-facing admin twin of the read-only Test Insights MCP tools agents already use, so debugging a flaky or failing grader doesn't require scraping Run transcripts.
GET /api/v1/admin/repositories/:repository_id/tests lists a repository's tests, filterable by state (recently_seen, failing, flaky, slow) and sortable by sort/direction (last_seen, last_failed, last_duration, failure_rate, avg_duration, p50_duration, p95_duration):
curl -G https://syrus.example.com/api/v1/admin/repositories/123/tests \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-d state=flaky -d sort=failure_rate -d direction=desc
GET /api/v1/admin/repositories/:repository_id/tests/:id returns one test's execution history, duration points, and related Run/Job references.
GET /api/v1/admin/jobs/:job_id/test_results returns the ingested test results for a Job's most recent Workflow with test data — compact by default; pass include_suites=true or include_slow_cases=true for more detail, grader_name to scope to one grader, and case_limit to select up to 100 detailed cases per test run. The default is 20; the response's truncation object reports omitted cases. Failure text fields are limited to 2 KiB each.
curl https://syrus.example.com/api/v1/admin/jobs/456/test_results \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
All three read from the same TestInsights::Query/TestInsights::Detail/ TestInsights::RunResults service objects the list_repository_test_insights, read_test_insight, and read_job_test_results MCP tools call, so the admin API and agent tools never see different data. With the plugin disabled, all three answer 404 with { "error": { "code": "plugin_disabled" } }.
Rename a Chat
POST /api/v1/app/chats/:id/rename (also accepted as PATCH) renames one of the authenticated user's chat sessions. name is required and must be 120 characters or fewer.
curl -X POST https://syrus.example.com/api/v1/app/chats/123/rename \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Release planning" }'
Delete a Chat
DELETE /api/v1/app/chats/:id soft-deletes a chat owned by the authenticated user. Group members cannot delete the shared conversation. Audit records such as messages, attachments, and proposals remain in the database, while search index entries and workspace files are cleaned up. There is no UI undo — use hide/unhide for reversible cleanup. The request is refused with 409 Conflict (error.code turn_in_flight) while a turn is actively running.
curl -X DELETE https://syrus.example.com/api/v1/app/chats/123 \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
Pin or Unpin a Chat
PATCH /api/v1/app/chats/:id accepts { "pinned": true } or { "chat": { "pinned": true } } for a chat owned by the authenticated user. Use false to unpin. The pin is shared session state, so group members cannot change it through the API, sidebar, or chat-agent tools. Pinned chats sort ahead of unpinned chats in the sidebar.
curl -X PATCH https://syrus.example.com/api/v1/app/chats/123 \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "chat": { "pinned": true } }'
Chat providers are pinned when a chat is created. Switching an existing chat to another configured provider uses POST /api/v1/app/chats/:id/switch_provider with { "provider": "codex" } or another enabled provider slug such as "claude", "agy", or "muse" so Syrus can run the provider rehydration flow before updating the stored provider. A provider is eligible only when its plugin is enabled and the authenticated user has the required credentials saved.
Branch a Chat
POST /api/v1/app/chats/:id/branch creates a new chat session for the authenticated user, attached to the same repository, with the current transcript copied into it. The response includes the new chat id and app_path for navigation.
curl -X POST https://syrus.example.com/api/v1/app/chats/123/branch \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
Share a Chat
POST /api/v1/app/chats/:id/share creates a stable same-instance share token for one of the authenticated user's chat sessions and returns share_url. Repeating the request returns the same link.
curl -X POST https://syrus.example.com/api/v1/app/chats/123/share \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
GET /api/v1/app/shared_chats/:token is available to any authenticated user on the same Syrus instance. It returns a read-only transcript payload with chat metadata and messages only; unknown tokens return 404.
curl https://syrus.example.com/api/v1/app/shared_chats/8b0d6a08-6f8d-4a2e-b2d8-0f2a2a2d8f6f \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
Hide or Restore a Chat
PATCH /api/v1/app/chats/:id/hide hides one of the authenticated user's chat sessions from the default sidebar, search, and chat listing surfaces. PATCH /api/v1/app/chats/:id/unhide restores it. Hide state belongs to the current participant, so hiding a group chat does not hide it for anyone else.
curl -X PATCH https://syrus.example.com/api/v1/app/chats/123/hide \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
GET /api/v1/app/settings/hidden_chats returns hidden chats ordered by most recently hidden first. Results include hidden_at, repository context, and an app_unhide_path for each row.
curl "https://syrus.example.com/api/v1/app/settings/hidden_chats?page=1" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
Search Chats
GET /api/v1/app/search searches the authenticated user's Jobs, Epics, and chat messages with one relevance-ranked result list. Pass q with at least two characters. Optional types[] values are job, epic, and chat; omit types[] to search all three. limit defaults to 30 and is capped at 100. Results include the result type, id, title, matched snippet, normalized rank, app navigation path, state and repository context for Jobs and Epics, and created_at. Job and Epic slugs such as JOB-123 and EPIC-56 return the matching record when it belongs to the authenticated user. Query terms use Google-style matching: foo bar requires both words in any order, "foo bar" searches for the exact phrase, and foo "bar baz" combines a required word with a required phrase. Chat results are grouped by conversation: the best matching message is the representative result, and grouped chat rows may include grouped_matches, total_match_count, and has_more_matches for inline expansion.
curl "https://syrus.example.com/api/v1/app/search?q=deploy&types[]=job&types[]=chat" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
GET /api/v1/app/chats/search searches the authenticated user's chats. Hidden chats are excluded. Pass q for full-text search, or omit it for a filter-only listing. The optional repository_id, epic_id, and job_id filters limit results to chats with matching attachments. Results are paginated with page and return chat-level cards with up to three matching message snippets.
curl "https://syrus.example.com/api/v1/app/chats/search?q=deploy&epic_id=456" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
GET /api/v1/app/chats/search/messages expands all matching messages for one chat and query:
curl "https://syrus.example.com/api/v1/app/chats/search/messages?chat_session_id=123&q=deploy" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
The app Epic detail API also supports dependency management when the authenticated user has write-tier access to the Epic's repository:
curl -X POST https://syrus.example.com/api/v1/app/epics/456/dependencies \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "depends_on_epic_id": 123 }'
POST /api/v1/app/epics/:id/dependencies returns the refreshed Epic detail payload, including dependencies and dependents. It rejects cycles and requires write access to both Epics' repositories. DELETE /api/v1/app/epics/:id/dependencies/:depends_on_epic_id removes that edge and is idempotent.
All app API mutations of an existing Epic—including edits, archive, ownership, state/start, and dependency changes—require the same effective repository write access. Read-tier access remains sufficient for viewing Epics. Reassignment additionally requires the selected owner to have effective write access to the Epic's repository.
Update Theme Preference
PATCH /api/v1/app/theme updates the authenticated user's app theme. Valid values are light, dark, and system. It can also set color_theme_id to one of the built-in color themes or a custom color theme owned by the authenticated user.
curl -X PATCH https://syrus.example.com/api/v1/app/theme \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "theme": "dark" }'
Manage Color Themes
GET /api/v1/app/themes lists built-in color themes plus custom, non-built-in themes owned by the authenticated user. GET /api/v1/app/themes/:id returns one selectable theme, plus a top-level contrast_warnings array (empty when the theme has no WCAG AA issues) so a draft theme's preview can show the same non-blocking contrast warnings the preview_theme chat tool returns, without gating the fetch on it. Theme payloads include:
Token hashes are abbreviated below; real create requests must include every theme token key for both modes.
{
"id": 123,
"slug": "copper-night-42",
"name": "Copper Night",
"built_in": false,
"position": 0,
"tokens": {
"light": { "brand": "#b6492e" },
"dark": { "brand": "#dba28b" }
}
}
Beyond the light/dark color hashes, every theme payload also includes five non-color token groups — shape (control/panel radius, border width), shadow (panel elevation), spacing (page/section spacing), density (control heights, table row height), and typography (font families, text sizes). Every theme, including ones created before this token expansion, gets Syrus's built-in defaults for these groups; the create/update endpoints below do not yet accept overrides for them.
POST /api/v1/app/themes creates a custom theme for the authenticated user. The request body must include name and a complete tokens object with light and dark token hashes covering the theme token schema.
curl -X POST https://syrus.example.com/api/v1/app/themes \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d @theme.json
PATCH /api/v1/app/themes/:id renames an owned custom theme and/or merges token overrides into its existing light/dark token hashes:
curl -X PATCH https://syrus.example.com/api/v1/app/themes/123 \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "theme": { "name": "Copper Night", "tokens": { "light": { "brand": "#0f766e" } } } }'
Create and update run the same WCAG AA contrast check used by the theming chat tools. A failing palette returns 422 with contrast_check_failed and an issues array naming the token pair, actual ratio, and required ratio.
DELETE /api/v1/app/themes/:id deletes an owned custom theme. If it is the user's active color theme, Syrus first falls back to the default built-in Terracotta theme. Built-in themes and another user's custom themes are not editable through these endpoints.
PATCH /api/v1/app/themes/reorder accepts ids, an ordered array of owned custom theme IDs, and persists their zero-based positions. Any custom themes owned by the user but omitted from ids are kept after the submitted order. Malformed IDs, duplicates, built-in theme IDs, and another user's theme IDs are rejected before positions are changed.
Manage Notifications
User-scoped clients can read and update the authenticated user's notifications through the app API. GET /api/v1/app/notifications returns newest-first paginated notifications and an unread_count envelope value. Each notification includes job_title when it is associated with a Job. Pass unread=true to list only unread notifications.
curl https://syrus.example.com/api/v1/app/notifications?unread=true \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
PATCH /api/v1/app/notifications/:id/mark_read marks one notification read and returns the updated notification plus unread_count. POST /api/v1/app/notifications/mark_all_read marks every unread notification for the authenticated user read and returns the refreshed list.
GET /api/v1/app/notification_preferences returns the authenticated user's merged notification category preferences. PATCH /api/v1/app/notification_preferences accepts a partial notification_preferences object and merges it into the saved preferences.
curl -X PATCH https://syrus.example.com/api/v1/app/notification_preferences \
-H "Authorization: Bearer $SYRUS_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "notification_preferences": { "epic_completed": true } }'
Manage Memories
User-scoped clients can manage agent memories through the app API with any user API token. GET /api/v1/app/memories returns paginated memories visible to that user, including their own memories and repository-published memories from repositories attached to them. It accepts scope, kind, q, and page query parameters.
Write endpoints are owner-only unless the authenticated user is an admin: POST /api/v1/app/memories, PATCH /api/v1/app/memories/:id, DELETE /api/v1/app/memories/:id, POST /api/v1/app/memories/:id/publish, and DELETE /api/v1/app/memories/:id/publish.
Worker Timeline
When the worker_timeline plugin is enabled, admin API clients can read the same multi-lane worker activity data the Worker Timeline sidebar page renders. Both endpoints require an admin API token; a disabled plugin answers a plugin_disabled error.
GET /api/v1/admin/worker_timeline/macro returns worker lanes and pending Workflows for a time window. Query params: from/to (ISO8601, default window is the last hour), repository_id, epic_id, job_id, hostname, status (comma-separated Workflow states), and job_type (user or system, with infra/infrastructure accepted as aliases for system).
curl "https://syrus.example.com/api/v1/admin/worker_timeline/macro?status=running" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"
GET /api/v1/admin/worker_timeline/workflow returns the ordered Step/Run waterfall for one Workflow, given ?id=<workflow_id>.
curl "https://syrus.example.com/api/v1/admin/worker_timeline/workflow?id=123" \
-H "Authorization: Bearer $SYRUS_API_TOKEN"