# Tasks API

9 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## POST /services/{serviceId}/run

Run a one-off command

Runs the command (argv, no shell) in a throwaway container of the service's current image with its environment, on the service's server, with the service's resource limits and a timeout (default 30 minutes). Requires `deployments:write`; audited. Returns 202 with the queued run; follow the output through `GET /task-runs/{runId}/logs?follow=true`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.
- `serviceId` (path, string, required)

Request body (application/json, required): RunCommandInputBody
  - `command` (string[] | null, required): Argv, run without a shell. Wrap in ["sh","-c",…] for shell syntax.
  - `timeoutSeconds` (integer (int64)): Default 1800 (30 minutes).

Response 202: Accepted (RunView)
  - `actorLabel` (string)
  - `command` (string[] | null, required)
  - `createdAt` (string (date-time), required)
  - `error` (string): Why the run failed, timed out or was skipped.
  - `exitCode` (integer (int64))
  - `finishedAt` (string (date-time))
  - `id` (string, required)
  - `logLines` (integer (int64), required)
  - `name` (string, required): The task's name, or "one-off" for a command.
  - `scheduledFor` (string (date-time))
  - `serviceId` (string, required)
  - `source` (string, required): cron, manual (Run now), one_off (run command) or a feature such as preview_seed.
  - `startedAt` (string (date-time))
  - `status` (enum: queued | running | succeeded | failed | timed_out | skipped, required)
  - `taskId` (string)
  - `timeoutSeconds` (integer (int64), required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## GET /services/{serviceId}/task-runs

Run history of a service's tasks and one-off commands

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)
- `taskId` (query, string (uuid)): Only the runs of this task.
- `limit` (query, integer (int64))

Response 200: OK (RunListOutputBody)
  - `items` (RunView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /services/{serviceId}/tasks

List a service's cron tasks

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Response 200: OK (TaskListOutputBody)
  - `items` (TaskView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /services/{serviceId}/tasks

Schedule a recurring command

A cron task runs its command in a throwaway container of the service's current image, with the service's environment and networks, on the service's server. The schedule is evaluated in `timezone` (IANA name, default UTC); one run at most per minute. Keeps the last 50 runs.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.
- `serviceId` (path, string, required)

Request body (application/json, required): CreateTaskInputBody
  - `command` (string[] | null, required): Argv, run without a shell in a container of the service's current image. Wrap in ["sh","-c",…] for shell syntax.
  - `concurrency` (enum: forbid | allow): forbid (default) skips a tick while the previous run is still active.
  - `enabled` (boolean): Default true.
  - `name` (string, required)
  - `schedule` (string, required): 5-field cron expression (minute hour day-of-month month day-of-week) or @hourly/@daily/@weekly/@monthly/@yearly, evaluated in timezone.
  - `timeoutSeconds` (integer (int64)): Default 1800 (30 minutes).
  - `timezone` (string): IANA time zone the schedule is evaluated in, e.g. Europe/Berlin. Default UTC.

Response 201: Created (TaskView)
  - `command` (string[] | null, required): Argv, run without a shell.
  - `concurrency` (enum: forbid | allow, required)
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `id` (string, required)
  - `lastRun` (RunView)
  - `lastRunAt` (string (date-time))
  - `name` (string, required)
  - `nextRunAt` (string (date-time))
  - `schedule` (string, required): 5-field cron expression or @macro, evaluated in timezone.
  - `serviceId` (string, required)
  - `timeoutSeconds` (integer (int64), required)
  - `timezone` (string, required): IANA time zone the schedule is evaluated in (default UTC). A daylight-saving gap skips that day's run; a repeated hour runs once.
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## GET /task-runs/{runId}

Get a task run

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `runId` (path, string, required)

Response 200: OK (RunView)
  - `actorLabel` (string)
  - `command` (string[] | null, required)
  - `createdAt` (string (date-time), required)
  - `error` (string): Why the run failed, timed out or was skipped.
  - `exitCode` (integer (int64))
  - `finishedAt` (string (date-time))
  - `id` (string, required)
  - `logLines` (integer (int64), required)
  - `name` (string, required): The task's name, or "one-off" for a command.
  - `scheduledFor` (string (date-time))
  - `serviceId` (string, required)
  - `source` (string, required): cron, manual (Run now), one_off (run command) or a feature such as preview_seed.
  - `startedAt` (string (date-time))
  - `status` (enum: queued | running | succeeded | failed | timed_out | skipped, required)
  - `taskId` (string)
  - `timeoutSeconds` (integer (int64), required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /task-schedules/preview

Describe a cron expression

Validates the expression and returns a plain-English summary (naming the time zone) and the next runs as UTC instants, computed in `timezone` (default UTC). An invalid expression or zone answers 200 with `valid: false` and the reason in `error`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): PreviewInputBody
  - `count` (integer (int64)): How many upcoming runs to return (default 5).
  - `schedule` (string, required)
  - `timezone` (string): IANA zone the schedule is evaluated in (default UTC).

Response 200: OK (SchedulePreview)
  - `description` (string): Human-readable summary naming the time zone.
  - `error` (string)
  - `nextRuns` (string (date-time)[] | null, required): The next runs as UTC instants.
  - `timezone` (string): The normalized zone the preview was computed in.
  - `valid` (boolean, required)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity; 500 Internal Server Error

## PATCH /tasks/{taskId}

Update a cron task

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `taskId` (path, string, required)

Request body (application/json, required): PatchTaskInputBody
  - `command` (string[])
  - `concurrency` (enum: forbid | allow)
  - `enabled` (boolean)
  - `name` (string)
  - `schedule` (string)
  - `timeoutSeconds` (integer (int64))
  - `timezone` (string): IANA zone; changing it recomputes the next run.

Response 200: OK (TaskView)
  - `command` (string[] | null, required): Argv, run without a shell.
  - `concurrency` (enum: forbid | allow, required)
  - `createdAt` (string (date-time), required)
  - `enabled` (boolean, required)
  - `id` (string, required)
  - `lastRun` (RunView)
  - `lastRunAt` (string (date-time))
  - `name` (string, required)
  - `nextRunAt` (string (date-time))
  - `schedule` (string, required): 5-field cron expression or @macro, evaluated in timezone.
  - `serviceId` (string, required)
  - `timeoutSeconds` (integer (int64), required)
  - `timezone` (string, required): IANA time zone the schedule is evaluated in (default UTC). A daylight-saving gap skips that day's run; a repeated hour runs once.
  - `updatedAt` (string (date-time), required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /tasks/{taskId}

Delete a cron task and its run history

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `taskId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /tasks/{taskId}/run

Run a cron task now

Starts a run immediately, whatever the schedule and concurrency policy. Returns 202 with the queued run; follow it through `GET /task-runs/{runId}/logs?follow=true`. 409 `not_deployed` / `server_offline` when it can not start.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `Idempotency-Key` (header, string): Client-chosen key (<=255 chars). A retry with the same key replays the original response for 24 hours.
- `taskId` (path, string, required)

Response 202: Accepted (RunView)
  - `actorLabel` (string)
  - `command` (string[] | null, required)
  - `createdAt` (string (date-time), required)
  - `error` (string): Why the run failed, timed out or was skipped.
  - `exitCode` (integer (int64))
  - `finishedAt` (string (date-time))
  - `id` (string, required)
  - `logLines` (integer (int64), required)
  - `name` (string, required): The task's name, or "one-off" for a command.
  - `scheduledFor` (string (date-time))
  - `serviceId` (string, required)
  - `source` (string, required): cron, manual (Run now), one_off (run command) or a feature such as preview_seed.
  - `startedAt` (string (date-time))
  - `status` (enum: queued | running | succeeded | failed | timed_out | skipped, required)
  - `taskId` (string)
  - `timeoutSeconds` (integer (int64), required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`not_deployed`); 422 Unprocessable Entity; 500 Internal Server Error
