# 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-token> → 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.
