# Tasks Autonomous agent scheduling. A task is a cron-driven, one-shot, or webhook-triggered execution in a headless service channel — no human participant in the loop. ## Configuration Global config keys (set via `PUT /admin/settings/:key`): | Key | Default | Description | |-----|---------|-------------| | `tasks.enabled` | `true` | Master kill switch for task scheduler | | `tasks.allow_personal` | `true` | Allow non-admin users to create tasks | | `tasks.max_concurrent` | `5` | Max tasks running simultaneously | | `tasks.personal_require_byok` | `false` | Personal tasks must use BYOK provider | Default budget ceilings (overridable per task): | Key | Default | |-----|---------| | `tasks.default_max_tokens` | `4096` | | `tasks.default_max_tool_calls` | `10` | | `tasks.default_max_wall_clock` | `300` (seconds) | ## Personal Tasks ### List My Tasks ``` GET /tasks ``` Returns tasks owned by the current user. **Auth:** Authenticated. ### Create Task ``` POST /tasks ``` ```json { "name": "Morning News Digest", "description": "Summarize top tech news", "task_type": "prompt", "persona_id": "uuid|null", "model_id": "claude-sonnet-4-20250514", "system_prompt": "You are a news summarizer.", "user_prompt": "Summarize the top 5 tech news stories from today.", "schedule": "@daily", "timezone": "America/New_York", "max_tokens": 4096, "max_tool_calls": 10, "max_wall_clock": 300, "output_mode": "channel", "webhook_url": "https://...", "provider_config_id": "uuid|null", "notify_on_complete": false, "notify_on_failure": true, "tool_grants": ["web_search", "url_fetch"] } ``` `task_type`: `prompt` (direct LLM execution), `workflow` (deferred — not yet implemented; creation is rejected at the API), or `action` (no LLM — relay/webhook only; requires `task.action` permission). `schedule`: cron expression, `once`, or `webhook`. Supported cron patterns: `@hourly`, `@daily`, `@weekly`, `*/N * * * *` (every N minutes), full 5-field cron via `robfig/cron/v3`. When `schedule` is `webhook`, the task fires only on inbound POST to its trigger URL. `output_mode`: `channel` (default — output stays in service channel), `note` (creates a note from the output), `webhook` (POST result to URL). **Auth:** `tasks.create` permission required. Additionally, `task.action` permission is required for `task_type: "action"`. ### Get Task ``` GET /tasks/:id ``` **Auth:** Owner or admin. ### Update Task ``` PUT /tasks/:id ``` Partial update. Accepted fields: `name`, `description`, `persona_id`, `model_id`, `system_prompt`, `user_prompt`, `workflow_id`, `tool_grants`, `schedule`, `timezone`, `is_active`, `max_tokens`, `max_tool_calls`, `max_wall_clock`, `output_mode`, `output_channel_id`, `webhook_url`, `provider_config_id`, `notify_on_complete`, `notify_on_failure`. **Auth:** `tasks.create` permission, must be owner. ### Delete Task ``` DELETE /tasks/:id ``` **Auth:** `tasks.create` permission, must be owner. ### Run Now (Manual Trigger) ``` POST /tasks/:id/run ``` Immediately executes the task regardless of schedule. Creates a new task run. Returns 409 if a run is already active or queued. **Auth:** `tasks.create` permission, must be owner. ### Kill Active Run ``` POST /tasks/:id/kill ``` Cancels the active run. Sets status to `cancelled`. **Auth:** `tasks.create` permission, must be owner. ### List Runs ``` GET /tasks/:id/runs ``` Returns run history for the task, most recent first. **Auth:** Owner or admin. ## Webhook Trigger (Inbound) External systems fire a task by POSTing to its trigger URL. ``` POST /api/v1/hooks/t/:token ``` The `:token` is a per-task secret generated at creation time (returned in the `trigger_token` field). The request body is stored as `trigger_payload` on the run record and forwarded to the executor: - **Prompt tasks:** payload is prepended to the user prompt as context. - **Action tasks:** payload is relayed to the outbound webhook as-is. - **Workflow tasks:** (deferred) payload becomes initial stage data. Returns `202 Accepted`: ```json { "triggered": true, "run_id": "uuid", "task_id": "uuid" } ``` Returns `409 Conflict` if a run is already active or queued. Returns `410 Gone` if the task is inactive. **Auth:** None (token-based). The trigger token *is* the credential. **Rate limiting:** Governed by global request rate limits. No per-task rate limiting in v0.28 — defer to a future version if needed. ## Team Tasks Team-scoped tasks visible to all team members, manageable by team admins. ### List Team Tasks (Member) ``` GET /teams/:teamId/tasks ``` Returns tasks scoped to the team. Read-only for members. **Auth:** Team member. ### List Team Task Runs (Member) ``` GET /teams/:teamId/tasks/:id/runs ``` **Auth:** Team member. ### Create Team Task (Admin) ``` POST /teams/:teamId/tasks ``` Same shape as personal task creation. Team scope is injected automatically. **Auth:** `tasks.create` permission, team admin. ### Update/Delete/Run/Kill Team Task ``` PUT /teams/:teamId/tasks/:id DELETE /teams/:teamId/tasks/:id POST /teams/:teamId/tasks/:id/run POST /teams/:teamId/tasks/:id/kill ``` **Auth:** `tasks.create` permission, team admin. ## Admin Tasks Platform-wide task management. ### List All Tasks ``` GET /admin/tasks ``` Returns all tasks across all users and teams. **Auth:** Platform admin. ### Admin Run/Kill/Delete ``` POST /admin/tasks/:id/run POST /admin/tasks/:id/kill DELETE /admin/tasks/:id ``` **Auth:** Platform admin. ## Task Object ```json { "id": "uuid", "owner_id": "uuid", "team_id": "uuid|null", "name": "Morning News Digest", "description": "...", "scope": "personal|team|global", "task_type": "prompt|workflow|action", "persona_id": "uuid|null", "model_id": "claude-sonnet-4-20250514", "system_prompt": "...", "user_prompt": "...", "workflow_id": "uuid|null", "tool_grants": ["web_search", "url_fetch"], "schedule": "@daily|once|webhook", "timezone": "America/New_York", "is_active": true, "trigger_token": "hex-string|null", "max_tokens": 4096, "max_tool_calls": 10, "max_wall_clock": 300, "output_mode": "channel|note|webhook", "output_channel_id": "uuid|null", "webhook_url": "https://...", "webhook_secret": "...", "provider_config_id": "uuid|null", "notify_on_complete": false, "notify_on_failure": true, "last_run_at": "...|null", "next_run_at": "...|null", "run_count": 42, "created_at": "...", "updated_at": "..." } ``` ## Task Run Object ```json { "id": "uuid", "task_id": "uuid", "channel_id": "uuid|null", "status": "queued|running|completed|failed|budget_exceeded|cancelled", "trigger_payload": "...|null", "started_at": "...", "completed_at": "...|null", "tokens_used": 1234, "tool_calls": 3, "wall_clock": 45, "error": "...|null" } ``` ## Execution The `TaskScheduler` is a background goroutine polling every 30 seconds. 1. Finds tasks where `next_run_at <= now` and `is_active = true` 2. Skips if an active run exists (`GetActiveRun` returns non-nil) 3. Adopts a queued run if one exists (from webhook trigger), otherwise creates a new run record 4. Marks `last_run_at` (records when execution started, not when it finished) 5. Creates or reuses a `service` channel (`output_channel_id`) 6. Persists the `user_prompt` as a message in the service channel (with trigger payload prepended if present) 7. For **prompt** tasks: runs `CoreToolLoop` (headless completion) 8. For **action** tasks: skips LLM, relays trigger payload to webhook 9. Enforces budgets: `max_tokens`, `max_tool_calls`, `max_wall_clock` 10. On budget breach: status = `budget_exceeded`, owner notified 11. On completion: calculates `next_run_at` **Workflow tasks:** Not yet implemented. The API rejects `task_type: "workflow"` at creation time. Reserved for a future version. **Provider resolution:** BYOK → team provider → global provider → routing policy. If `tasks.personal_require_byok` is true, personal tasks that don't have a BYOK provider fail at step 7. **Action tasks** skip steps 6–9. They create the service channel, fire the outbound webhook with the trigger payload, and complete. ## Webhooks (Outbound) Tasks with `webhook_url` fire a POST on completion: ```json { "task_id": "uuid", "run_id": "uuid", "task_name": "Morning News Digest", "channel_id": "uuid", "status": "completed|failed|budget_exceeded", "completed_at": "2025-03-11T12:00:00Z", "output": "...", "tokens_used": 1234, "error": "...|null" } ``` Signed with HMAC-SHA256 using `webhook_secret` in the `X-Switchboard-Signature` header. Retry: 3 attempts, exponential backoff (1s, 5s, 25s). ## Task Chaining Task A's outbound `webhook_url` can be Task B's trigger URL: ``` Task A (webhook_url) → POST /api/v1/hooks/t/ → Task B runs ``` This enables multi-step pipelines: CI failure → summarize with LLM → post to Slack. Each link is a separate task with its own type, budget, and notification preferences.