:title
11. Tasks and jobs

:content
A task runs a Capy handler outside the current request. A job runs an operating-system command. Both return immediately with a handle that the caller can inspect later.

## Read status data

Task status is a `dval` because fields can vary with the state:

:example capy render
function state_label(status : dval) string
{
	-> string(status.state, fallback: "missing")
}

function RENDER()
{
	print(state_label(state: "queued"), "\n")
}

:output
queued

:content
Real task states are `queued`, `running`, `succeeded`, `failed`, `canceled`, or `missing`.

## Define a task handler

A named task handler receives task input through `props`. It also binds the worker's request implicitly:

:example capy render
function TASK: STORE(props : dval)
{
	var key := string(props.key)
	if key == ""
	{
		trap()
	}
	// Store the application result under key.
}

function RENDER()
{
	print("task handler ready\n")
}

:content

A task runs in another process, so its props are serialized and the worker gets a copy. The worker does not receive the caller's request, session, connections, open handles, or memory state.

## Run a task

`task_execute` starts the task in a process of its own right away and returns. The timeout is mandatory: when it is up, the process is ended whether the handler finished or not:

:example capy cli
function RENDER()
{
	var started := task_execute("/doc/examples/task_worker.capy:STORE", 30, key: "report-42")
	print(string(started.error, fallback: "started"), "\n")
}

:content

The result is `{pid}` when the process started and `{error}` when it could not, for example `invalid_target` for an unknown unit. Nothing waits for the task and nothing reports its result; it writes what it produces to storage the application owns.

The queued form, `task`, returns an opaque string ID after Bearer admits the task; `task_status(id)` polls it and `task_await(id, timeout_ms)` waits for a state:

:example capy render
function show_failure(id : string)
{
	var status := task_await(id, 5000)
	if string(status.state) == "failed"
	{
		print(string(status.failure_code, fallback: "unknown"))
	}
}

function RENDER()
{
	print("failure helper ready\n")
}

:content

A wait timeout does not cancel active work. `task_cancel(id)` requests cancellation, but a running task can finish first. Cancellation does not undo an external effect.

A successful task means that its handler finished. Task status does not carry an application result. Store results in application-owned persistent storage.

## Run a command

The two-argument `shell_exec` form starts a bounded background operating-system job:

:example capy render
function RENDER()
{
	var job := shell_exec("printf ready", background: true, timeout_ms: 1000)
	var status := job_await(job, 3000)
	print(string(status.state, fallback: "missing"), "\n")
}

:content

Use `job_status`, `job_await`, `job_result`, and `job_cancel` with the returned handle. Always set a deliberate command timeout. Validate the final state before reading result data.

The shell interprets the command string. Do not build it from request data or other untrusted input.

## Failure and retries

Bearer does not retry a failed task automatically. Submit a new task only when the application can repeat the operation safely. Use an application idempotency key for work that changes external state.

Do not wait for long work in a page request unless the response requires it. Treat task IDs and job handles as opaque values.

## Keep task input small

Pass a `dval` that contains only stable identifiers and simple options. Do not pass the whole request to a task. Request data can contain cookies, headers, and uploaded-file metadata that the task does not need.

Use a database row, file path, or application key when work needs large input. Validate that reference again inside the task. A task can run after the user request has ended.

Use a visible status page for long work. It should show queued, running, done, failed, and canceled states in user terms.

## Cache across requests

The runtime keeps a key-value cache in its own memory, shared by every worker and alive as long as the runtime runs. `kv_cache()` returns a value when the cache holds it and otherwise calls your function, stores the result, and returns it:

:example capy render
function RENDER()
{
	var posts := kv_cache("recent-posts", function() dval
	{
		-> { count: 3 }
	}, 60)
	print(s64(posts.count), "\n")
}

:output
3

:content
`kv_get()`, `kv_set()`, `kv_delete()`, and `kv_incr()` are the plain operations; `kv_delete()` is how a write invalidates what it changed. Values are copied in and out, so a cached `dval` is never shared with the code that stored it. The cache has a fixed size, set by `KV_CACHE_BYTES`, and drops the least recently used entries when it is full.

:see-group
guides

:see
task
task_status
task_await
task_cancel
shell_exec
job_status
job_result
job_await
job_cancel
capy-08-dynamic-values
capy-13-errors-testing-and-style
