# 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), `action`
(no LLM — relay/webhook only; requires `task.action` permission),
or `system` (v0.28.6 — built-in Go function, admin-only).

System tasks require `system_function` — a registered function name
from the platform's Go function registry. No LLM, no provider
resolution, no channel needed. The function receives the store set
and returns a result string. Designed for core platform ops
(`session_cleanup`, `staleness_check`, `retention_sweep`, `health_prune`)
that must not break from bad user code. Permanent track — not replaced
by Starlark in v0.29.0.

`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.

### Task-to-Task Chaining (v0.28.6)

Tasks can trigger other tasks by setting the `webhook_url` of Task A
to the trigger URL of Task B:

1. Create **Task B** with `schedule: "webhook"`. Note the trigger URL
   from the response (`/api/v1/hooks/t/{trigger_token}`).
2. Create **Task A** with `output_mode: "webhook"` and set `webhook_url`
   to Task B's trigger URL.
3. When Task A completes, its output is POSTed to Task B's trigger URL.
   Task B starts with Task A's output as its `trigger_payload`.

This enables multi-step pipelines without external orchestration:
- Task A: "Summarize today's news" (prompt, cron: 6am)
- Task B: "Format and post to Slack" (action, webhook-triggered)

The `webhook_secret` on Task A signs outbound payloads with HMAC-SHA256.
Task B's trigger endpoint does not currently verify signatures (the
trigger token itself is the auth mechanism).

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

### System Functions (v0.28.6)

```
GET /admin/system-functions → { "data": [SystemFuncInfo, ...] }
```

Returns all registered built-in system function names and descriptions.
Used by the admin UI to populate the function dropdown when creating
system tasks.

**SystemFuncInfo:**

```json
{
  "name": "session_cleanup",
  "description": "Remove expired anonymous visitor sessions"
}
```

Built-in functions (v0.28.6): `session_cleanup`, `staleness_check`,
`retention_sweep`, `health_prune`.

**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|system",
  "system_function": "session_cleanup",
  "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.
