# 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:<peer>,<peer>` 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 `"<n>%"` | — | 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.<name>`
- 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