# Orca Jobspec Reference This document is the complete reference for the Orca jobspec format — the Markdown-with-frontmatter specification that describes workloads. > **Canonical format (v0.9)**: Orca uses Markdown with YAML frontmatter > as the canonical jobspec format (R-013/R-014). The legacy HCL format > is supported via an adapter during the migration window but is > deprecated (see [HCL jobspec](#deprecated-hcl-jobspec) below). ## File formats The `orca job run` command dispatches by file extension: | Extension | Parser | Body | |-----------|--------|------| | `.md` | `ParseMarkdown` (canonical) | Verbatim after closing `---` (R-015 byte-exact) | | `.yaml` / `.yml` | `parseYAMLFile` | Whole file as frontmatter; body empty | | `.hcl` | `ParseHCL` (legacy adapter) | Empty (deprecated) | ## Minimal example ```yaml --- kind: Job name: my-job runtime: one_of: process command: /bin/echo hello --- # My Job This body is preserved byte-exact and carried to the target node. ``` ## Top-level keys | Key | Type | Default | Required | Notes | |-----|------|---------|----------|-------| | `orca-spec-version` | string | `""` | no | Free-form version tag (e.g. `"1"`) | | `kind` | enum | — | **yes** | One of `Job`, `Service`, `DaemonSet` | | `name` | string | — | **yes** | Workload name (trimmed, non-empty) | | `count` | int | `1` | no | Job: must be 1; Service: ≥1; DaemonSet: not allowed | | `runtime` | block | nil | see kinds | Runtime block (or per-task runtimes in a task group) | | `ports` | block list | nil | Service: **yes** | Array of port mappings | | `env` | block map | nil | no | Environment variables | | `secrets` | inline/block list | nil | no | Secret names (resolution in v0.11) | | `volumes` | block list | nil | no | Volume mounts | | `restart` | block | nil | Service/DaemonSet: **yes** | Restart policy | | `update` | block | nil | Service: **yes** | Update strategy | | `service` | block | nil | no | Traefik route definition (implied for Service; not allowed for Job/DaemonSet) | | `health` | block | nil | Service: **yes** | Health check | | `lifecycle` | block | nil | no | Pre-stop / post-start hooks | | `constraints` | list | nil | no | CEL expressions (node selection) | | `affinity` | block list | nil | no | Co-location / anti-affinity rules | | `tasks` | block list | nil | no | Task group (multi-process alloc) | | `timeout` | duration string | `""` | no | Job timeout | | `schedule` | block | nil | DaemonSet: **yes** | Schedule mode | ## Kinds ### `Job` A one-shot batch task. Runs once and exits. - `count` must be 1 (or unset). Use `Service` for replicas. - `service` block is **not allowed** (no Traefik route for Jobs). - `restart` optional (defaults to `never` / `on-failure`). - `timeout` optional. **Example**: ```yaml --- kind: Job name: data-migration runtime: one_of: process command: /usr/bin/python3 migrate.py timeout: 300s env: DB_URL: postgres://localhost/mydb --- ``` ### `Service` A long-running, load-balanced workload with a Traefik route. - `count` ≥ 1 (number of replicas). - `ports` required (at least one). - `restart` required; `mode` one of `service`, `on-failure`, `never`. - `update` required; `strategy` one of `rolling`, `canary`, `blue-green`. - `runtime` required (or a task group with per-task runtimes). - `health` required (Traefik routing requires health checks). - `service` block optional (implied for Service; use for `bind` override). - `service.bind` if present must be a valid IP (`127.0.0.1` = TCP opt-in; default = Unix socket). **Example**: ```yaml --- kind: Service name: web count: 3 runtime: one_of: process command: /usr/bin/httpd ports: - name: http port: 8080 restart: mode: service attempts: 5 delay: 2s update: strategy: rolling max_parallel: 1 health: check_type: http interval: 5s timeout: 1s unhealthy_threshold: 2 constraints: - node.role == "web" --- ``` ### `DaemonSet` A workload that runs on every matching node. - `schedule` required; `mode` one of `every-node`, `matching`, `mandatory`. - `ports` **not allowed** (no Traefik route by default). - `count` **not allowed** (implicit = matching nodes). - `restart` required. **Example**: ```yaml --- kind: DaemonSet name: log-shipper schedule: mode: every-node runtime: one_of: process command: /usr/bin/fluent-bit restart: mode: service --- ``` ## Block reference ### `runtime` The runtime backend for the workload. | Field | Key | Type | Default | Notes | |-------|-----|------|---------|-------| | `one_of` | `one_of` | string | — | Runtime type (see below) | | `image` | `image` | string | `""` | Container image (for `podman`) | | `command` | `command` | string | — | ExecStart command | **Supported runtime types** (`one_of`): | Type | Description | Requires | |------|-------------|----------| | `process` | Direct process execution via systemd (default) | systemd on target | | `wasm` / `wasmtime` | WASM via wasmtime CLI (apt-installed on peer, SSH exec) | wasmtime on target | | `podman` | Container via podman | podman on target | | `pve-vm` | Proxmox VM via `qm` | Proxmox node | | `pve-ct` | Proxmox container via `pct` | Proxmox node | | `proxmox` | Alias for Proxmox runtime | Proxmox node | An empty/missing `Runtime` or `OneOf` is runtime-agnostic (always fits the runtime axis in the scheduler). ### `ports` Array of port mappings. Required for `Service`. | Field | Key | Type | Default | Notes | |-------|-----|------|---------|-------| | `name` | `name` | string | — | Port name (used in socket path) | | `port` | `port` | int | — | Container port | | `host_port` | `host_port` | int | `0` | Host port | | `protocol` | `protocol` | string | `""` | Protocol (e.g. `tcp`) | | `host_ip` | `host_ip` | string | `""` | Host IP | **Example**: ```yaml ports: - name: http port: 8080 host_port: 80 protocol: tcp - name: https port: 8443 host_port: 443 ``` ### `env` Environment variables. Scalar values or secret references. ```yaml env: FOO: bar BAZ: "qux" SECRET_REF: from: "secret:db-password" INLINE: {from: "secret:token"} ``` > Secret resolution (`from: "secret:..."`) lands in v0.11-P03. The > parser stores the reference; the emitter will emit > `EnvironmentFile=`/`LoadCredential=` in v0.11. ### `secrets` List of secret names. Inline array or block list. ```yaml secrets: ["db-password", "api-token"] # or secrets: - db-password - api-token ``` ### `volumes` Array of volume mounts. | Field | Key | Type | Default | Notes | |-------|-----|------|---------|-------| | `name` | `name` | string | — | Volume name | | `type` | `type` | string | — | Volume type (e.g. `host`) | | `source` | `source` | string | — | Source path (or `replicate:,` for Syncthing) | | `target` | `target` | string | — | Mount target | | `read_only` | `read_only` | bool | `false` | Read-only mount (`true`/`yes`/`on`/`1`) | **Example**: ```yaml volumes: - name: data type: host source: /data target: /data read_only: true ``` ### `restart` Restart policy. | Field | Key | Type | Default | Notes | |-------|-----|------|---------|-------| | `mode` | `mode` | enum | — | `never`, `on-failure`, `service` | | `attempts` / `max_retries` | `attempts` or `max_retries` | int | `0` | Max retries (both keys accepted) | | `delay` | `delay` | duration string | `""` | Retry delay (e.g. `2s`) | ### `update` Update strategy. Required for `Service`. | Field | Key | Type | Default | Notes | |-------|-----|------|---------|-------| | `strategy` | `strategy` | enum | — | `rolling`, `canary`, `blue-green` | | `max_surge` | `max_surge` | int | `0` | Max surge | | `max_parallel` | `max_parallel` | int | `1` (clamped to `count`) | Max parallel updates | | `min_healthy_time` | `min_healthy_time` | duration | `""` | Min time healthy before next batch | | `healthy_deadline` | `healthy_deadline` | duration | `""` | Deadline for health | | `canary` | `canary` | int or `"%"` | — | Canary size (int count or percentage) | | `auto_promote` | `auto_promote` | bool | `false` | Auto-promote canary (`true`/`yes`/`on`/`1`) | **Strategies**: - **rolling**: batches of `max_parallel`, each batch waits for healthy. - **canary**: canary batch first, then `promote` (manual or `auto_promote`), then remaining in `max_parallel` batches. - **blue-green**: all new allocs start in parallel, wait healthy, then `cutover`. > Transactional update execution lands in v0.11-P10. The current > emitter computes the plan; execution is a v0.11 deliverable. ### `service` Traefik route definition. Implied for `Service`; not allowed for `Job`/`DaemonSet`. See [docs/ingress.md](ingress.md) for details. | Field | Key | Type | Default | Notes | |-------|-----|------|---------|-------| | `name` | `name` | string | — | Service name | | `port` | `port` | int | — | Service port | | `bind` | `bind` | string (IP) | `""` | Bind mode: empty = Unix socket (default); `127.0.0.1` = TCP opt-in (R-007) | | `host` | `host` | string | `""` | Host (stored, not yet consumed by emitter) | | `route_id` | `route_id` | string | `""` | Route ID (stored, not yet consumed by emitter) | ### `health` Health check. Required for `Service`. | Field | Key | Type | Default | Notes | |-------|-----|------|---------|-------| | `check_type` | `check_type` | string | — | Check type (e.g. `http`) | | `interval` | `interval` | duration string | — | Check interval (e.g. `5s`) | | `timeout` | `timeout` | duration string | — | Check timeout | | `unhealthy_threshold` | `unhealthy_threshold` | int | `0` | Failures before unhealthy | Maps to Traefik `healthCheck` stanza (`path: /healthz`). ### `lifecycle` Lifecycle hooks. Maps to systemd `ExecStartPost` / `ExecStop`. | Field | Key | Type | Default | systemd mapping | |-------|-----|------|---------|-----------------| | `post_start` | `post_start` | string list | nil | `ExecStartPost=` (runs after main starts) | | `pre_stop` | `pre_stop` | string list | nil | `ExecStop=` (runs before kill) | **Example**: ```yaml lifecycle: pre_stop: - /bin/sh -c 'sleep 5' - /usr/local/bin/drain.sh post_start: - /usr/local/bin/warm-cache.sh ``` ### `constraints` CEL-subset expressions for node selection. Inline array or block list. ```yaml constraints: - node.role == "web" - region == "us" # or inline constraints: ['node.role == "web"', 'region == "us"'] ``` **CEL subset grammar** (hand-rolled, no CEL dependency): - Node attributes: `node.hostname`, `node.kind`, `node.cpus`, `node.memory`, `node.tags`, `node.runtimes` - Bare identifiers: equivalent to `node.` - Literals: string (`"..."`), int - Comparisons: `==`, `!=`, `>=`, `<=`, `>`, `<` - Membership: `in`, `not in` - Boolean: `and`, `or`, `not`, parentheses - Anything outside the subset returns an error (node skipped, not silently mis-evaluated) ### `affinity` Co-location / anti-affinity rules. ```yaml affinity: - target: zone == "a" weight: 80 - target: web weight: -50 # anti-affinity (negative weight) ``` - `target`: CEL expression or bare workload name (for name-based co-location). - `weight`: positive = co-locate, negative = anti-affinity. - Affinity is a **hint** (not a gate); evaluation failures are ignored. ### `tasks` (task group) Multi-process alloc (P06). When `tasks` is non-empty, the alloc runs multiple processes, each as its own systemd unit, grouped under a systemd target. ```yaml tasks: - name: app runtime: one_of: process command: /usr/bin/httpd -f env: LOG_LEVEL: debug - name: sidecar runtime: one_of: wasm command: /bin/wasm-runner sidecar.wasm ``` - A task with no `runtime:` inherits the top-level `spec.Runtime`. - Each task can have its own `env:` overlay. - `command` falls back: `task.Command` → `task.Runtime.Command` → `spec.Runtime.Command`. - Task names must be unique within the group. ## Kinds matrix | Feature | Job | Service | DaemonSet | |---------|-----|---------|-----------| | `count` | must be 1 | ≥ 1 | not allowed | | `ports` | optional | **required** | not allowed | | `service` block | not allowed | optional (implied) | not allowed | | `restart` | optional | **required** | **required** | | `update` | optional | **required** | optional | | `health` | optional | **required** | optional | | `runtime` | optional | **required** (or task group) | optional | | `schedule` | optional | optional | **required** | | `tasks` | optional | optional | optional | | Traefik route | no | yes (implied) | no (by default) | ## Body semantics The body after the closing `---` is preserved **byte-exact** (R-015) — including trailing newlines, CRLF, BOM in body, and `---` inside code fences. The body is carried verbatim to the target node. It is not interpreted as commands/scripts by the parser today. ## Deprecated: HCL jobspec The legacy HCL jobspec format is supported via an adapter during the migration window. It is deprecated in v0.9 and will be removed in v0.11. ```hcl job "hello-orca" { } task "greet" { command = "/bin/echo" args = ["hello", "from", "orca"] } ``` The adapter converts this to a `*WorkloadSpec{Kind: "Job", Name: "hello-orca", Count: 1, Runtime: {OneOf: "process", Command: "/bin/echo"}}`. Use `.md` for all new jobspecs. ## See also - [docs/cli.md](cli.md) — CLI reference (including `orca job run`) - [docs/ingress.md](ingress.md) — Traefik ingress configuration - [examples/full-stack/](../examples/full-stack/) — Full-stack example jobspecs