Optio has one user-facing concept — a Task — with one attribute that flips the pipeline behind it: does the task have a repo? Everything else (prompts, triggers, agent config, run history, logs, costs) is shared.
This guide covers the two flavors, when to use each, and how the unified /api/tasks HTTP layer presents them.
A Repo Task targets a specific repository and ends by opening a pull request. The pipeline:
- Find or spin up an isolated Kubernetes pod for the repo (pod-per-repo).
- Create a git worktree for the task — multiple tasks can run concurrently on one pod.
- Run the configured agent (Claude Code, Codex, Copilot, Gemini, OpenCode) with the rendered prompt.
- Stream structured logs back to the web UI in real time.
- The agent stops after opening a PR — it does not block on CI.
- The PR watcher tracks CI checks, review status, and merge state.
- If enabled, auto-trigger the code-review agent on CI pass or PR open.
- If enabled, auto-resume the agent when reviewers request changes.
- Auto-complete on merge; auto-fail on close.
Use a Repo Task when the work produces code: bug fixes, features, refactors, dependency bumps, doc edits — anything that wants to land as a reviewed PR.
A Standalone Task runs an agent in an isolated pod with no repo checkout. It produces logs and side effects: querying Slack, writing to a database, posting a report, calling an MCP server, triaging a ticket queue.
Use a Standalone Task when the work is not code: scheduled reports, cron-driven triage, webhook responses, on-demand operational scripts.
The user-facing flavors are backed by three internal types. The distinction is whether the row is a blueprint (definition that spawns runs) or a one-off run:
| Type | Backing table | Purpose | Has runs | Has triggers |
|---|---|---|---|---|
repo-task |
tasks |
Ad-hoc one-time Repo Task | No (it is a run) | No |
repo-blueprint |
task_configs |
Reusable Repo Task definition that spawns tasks |
Yes | Yes |
standalone |
workflows |
Standalone Task definition that spawns workflow_runs |
Yes | Yes |
A blueprint plus a fired trigger produces a run. The same trigger types (manual, schedule, webhook, ticket) work for both repo-blueprint and standalone because the trigger table is polymorphic — see Triggers below.
Backend-naming note. The schema still says
workflows,workflow_runs, andworkflow_triggersfor historical reasons. User-facing copy is always "Task" / "Repo Task" / "Standalone Task". Routes follow the same rule:/api/tasksis the canonical surface;/api/jobs/*and/api/task-configs/*are back-compat aliases.
All three types are reachable through one polymorphic resource. The server resolves an ID across all three tables; UUIDs are globally unique so there is no collision.
| Endpoint | Purpose |
|---|---|
GET /api/tasks?type=repo-task|repo-blueprint|standalone|all |
Unified list, filterable by type |
POST /api/tasks |
Create. Body takes { type, ... } and dispatches to the right service |
GET /api/tasks/:id |
Resolve across tables; returns the native row tagged with type |
GET/POST /api/tasks/:id/runs[/:runId] |
List/start runs (spawned tasks for blueprints, workflow_runs for standalone, 405 for ad-hoc) |
GET/POST/PATCH/DELETE /api/tasks/:id/triggers[/:triggerId] |
Manage triggers (405 for ad-hoc repo-task) |
The resolver lives in apps/api/src/services/unified-task-service.ts (resolveAnyTaskById) and checks tasks → task_configs → workflows in order. The polymorphic routes are in apps/api/src/routes/tasks-unified.ts.
Legacy /api/jobs/* and /api/task-configs/* endpoints still work as thin aliases.
The web UI hides the schema split entirely.
/tasks— the main hub.- Repo Tasks tab: ad-hoc and blueprint-spawned
tasksruns. Bulk actions, state filters, real-time WebSocket updates. - Standalone tab:
workflows(blueprints) with their runs and triggers. One-click "Run Now." - Issues tab: GitHub Issues across connected repos. "Assign to Optio" creates an ad-hoc Repo Task.
- Pull Requests tab: PR browser with CI / review / merge tracking.
- Repo Tasks tab: ad-hoc and blueprint-spawned
/tasks/scheduled— managerepo-blueprintrows: schedule, webhook, ticket, manual triggers; pause/resume; run-now./jobs,/jobs/:id,/jobs/:id/runs/:runId— Standalone detail and run views (the legacy "jobs" route names live on under the hood).
Triggers live in one polymorphic table, workflow_triggers, keyed by (target_type, target_id). target_type is "job" (Standalone) or "task_config" (Repo blueprint). Trigger types: manual, schedule (cron), webhook, ticket.
The workflow-trigger-worker polls due schedule triggers every 60 seconds (OPTIO_WORKFLOW_TRIGGER_INTERVAL) and dispatches:
target_type="job"→workflowService.createWorkflowRun()→ spawns aworkflow_runsrow.target_type="task_config"→taskConfigService.instantiateTask()→ renders the blueprint's prompt with trigger params, creates atasksrow, transitions it toqueued, and enqueues the BullMQ job.
Both blueprint types use the same template engine: {{param}} substitution and {{#if param}}...{{/if}} blocks. Templates render lazily at trigger-firing time, so values from the trigger payload (cron-formatted timestamps, webhook bodies, ticket fields) substitute into the prompt before the agent ever sees it.
Reusable templates live in prompt_templates with a kind discriminator (prompt / review / job / task). Template precedence: repo override → global default → hardcoded fallback.
| Concern | Service | Routes |
|---|---|---|
| Polymorphic resolution | services/unified-task-service.ts |
routes/tasks-unified.ts |
| Repo Task runs | services/task-service.ts, workers/task-worker.ts |
routes/tasks.ts |
| Repo Task blueprints | services/task-config-service.ts |
routes/task-configs.ts |
| Standalone Tasks | services/workflow-service.ts, workers/workflow-worker.ts |
routes/workflows.ts (also mounted as /api/jobs) |
| Triggers | services/workflow-trigger-service.ts, workers/workflow-trigger-worker.ts |
included in the above |
| Templates | services/prompt-template-service.ts |
routes/prompt-templates.ts |
State changes for both Repo Task runs and Standalone runs flow through the reconciliation control plane when enabled.