# orca.toml

> The orca.toml reference: its tables, every key of [defaults] and [pipelines.<name>] with its type and default, worker pools in [pools.<name>], fills, how many runs at once, requests and grants, what orca register refuses, which file it reads, and orca config show, check and export.

[morca](https://morca.dev/)

[](https://morca.dev/search "Search")

`orca.toml` sits at the root of a project's repository and says how each pipeline runs: where, on what, how much it uses and how it fills. The pipeline's code still says what it does. `orca register` reads the file, checks every key, and records each value with where it came from.

## An example

```
[defaults]
placement = "ec2"
head_type = "t4g.medium"

[pipelines.gharchive]
worker_type = "c7g.xlarge"
workers = "0..4"
max_runs = 2
env = { DISTA_WORKER_SLOTS = "4", GHARCHIVE_PARALLEL = "16" }
fills = "all"                # fill the whole range by itself
fill_max_runs = 2
fill_budget_per_day = 5.0    # estimated US dollars a UTC day
fill_window = "22..6"        # UTC hours fill runs start in
fill_lag = "2h"              # a day fills 2 hours after it ends

[pipelines.mojo_repos]
placement = "worker:lab"
secrets = { GITHUB_TOKEN = "orca/github-token" }
cpus = "1..2"                # starts once 1 CPU and 0.5 GB fit,
memory_gb = "0.5..2"         # and is given up to 2 of each

[pools.lab]
provider = "hetzner"
machines = [{ type = "cax31", wait = 600 }, { type = "cpx32" }]
locations = ["hel1", "nbg1"]
max_machines = 3
```

The full reference, with every rule, is [docs/orca-toml.md](https://github.com/morca-dev/morca/blob/main/docs/orca-toml.md); the code that reads the file is [config.mojo](https://github.com/morca-dev/morca/blob/main/src/orca/config.mojo) and [pool\_config.mojo](https://github.com/morca-dev/morca/blob/main/src/orca/pool%5Fconfig.mojo).

## Tables

| Table | What it is |
| --- | --- |
| \[defaults\] | values for every pipeline the file names; pipelines the file leaves out don't get them |
| \[pipelines.<name>\] | one pipeline's own values, by the name its describe gives; an empty table puts the pipeline in the file with \[defaults\] |
| \[pools.<name>\] | a pool of remote workers that morca grows and shrinks itself |
| \[apps.<name>\] | from 0.26.0: a long-running binary of the image beside the UI, such as the census console, and what the stack gives it |
| \[stack\] | reserved for orca aws up, which doesn't read it yet; it is accepted and not checked |

Any other table, and any key at the top level, is refused. morca reads the part of TOML 1.0 a config needs: comments, dotted and quoted keys, basic and literal strings, decimal integers, floats, booleans, arrays (over several lines, with a trailing comma), inline tables and arrays of them. Multi-line strings, dates, `[[arrays of tables]]`, hexadecimal, octal and binary integers, `inf` and `nan` are refused with a message that says so, as is a key or table defined twice.

## \[pipelines.<name>\]

Every key below can go in `[defaults]` or in a pipeline's own table. A value comes from the first of these that sets it: a flag to `orca register` (recorded as an override), the pipeline's table, `[defaults]`, the environment, then the code. `env` and `secrets` merge one name at a time. `image` and `--spot` stay flags only, and no flag sets the `fill*` keys or `learn`.

### Placement and size

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| placement | "default", "control", "ec2" or "worker:<pool>" | default (the scheduler's ORCA\_RUNNER) | Which machine starts the pipeline's runs. control is the scheduler's host; worker:<pool> is a remote worker of that pool. |
| head\_type | string, an instance type | ORCA\_EC2\_HEAD\_TYPE, else c7g.large | The head instance's type, used only on EC2. |
| worker\_type | string, an instance type | ORCA\_EC2\_WORKER\_TYPE, else c7g.xlarge | The worker instances' type, used only on EC2. |
| workers | integer N (at least 1), or "MIN..MAX" | the code's workers, else 2; 0 without tasks | Dista workers a run gets. With a range the run scales between the two. On worker:<pool>, the extra machines one run may spread over. |
| worker\_procs | integer 1 to 256, or "auto" | 1 | Dista processes per worker machine; auto is one per vCPU. |
| max\_runs | integer 1 to 10000, "auto" or "auto..N" | the code's max\_runs, else 1 | Live runs of the pipeline at once, overriding the code. auto follows the host's room, up to 64 or N. |
| keys\_per\_run | integer 1 to 10000 | the code's keys\_per\_run, else 1 | Keys one run takes: a backfill's runs, a sensor's runs above 1, and fill runs. |

[placement.md](https://github.com/morca-dev/morca/blob/main/docs/placement.md) describes the four placements, and [Remote workers](https://morca.dev/docs/workers) covers `worker:<pool>`.

### Requests

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| cpus | integer 1 to 1024, "MIN..MAX", "auto" or "auto..MAX" | none: no request | Whole CPUs a run needs to start and can use. auto takes the request learned from its runs. |
| memory\_gb | number over 0 up to 65536, "MIN..MAX", "auto" or "auto..MAX" | none: no request | GB of memory a run needs to start and can use; decimals are allowed. |
| learn | boolean | true | false keeps auto at no request, though the recommendation is still shown. |

### Fills

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| fills | "all" or "since\_registration" | since\_registration | all makes the scheduler fill the pipeline's whole range by itself. |
| fill\_order | "oldest" or "newest" | oldest | Which end of the range fills first. |
| fill\_max\_runs | integer 1 to 10000; "auto" or "auto..N" from 0.24.0 | 1 | Fill runs at once, on top of max\_runs. auto follows the host's room, up to 64 or N. |
| fill\_budget\_per\_day | number 0 to 100000, US dollars | none | Estimated spend of fill runs per UTC day. Fill runs on EC2 or Kubernetes don't start without it. |
| fill\_window | "A..B", UTC hours: A 0 to 23, B 1 to 24 | "0..24", all day | The hours fill runs may start in. "22..6" wraps past midnight. |
| fill\_lag | a duration: whole s, m, h or d up to 31d, or 0 | 0 | How long after its end a partition is filled, for sources that publish late. |

### Secrets and environment

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| secrets | table of NAME = "orca/<id>" | none | Secret IDs, never values, read when a run starts and passed as NAME. A --secret flag replaces the whole set. |
| env | table of NAME = "value" | none | Variables for this pipeline's runs only, on every runner. Numbers and booleans become text; "auto" sizes from the grant. |

## How many at once

`max_runs = "auto"` starts another run of the pipeline whenever its request fits in what this host's running runs weren't granted, up to a ceiling of 64; `"auto..N"` sets the ceiling to N. A bigger host then runs more at once with no change to the file: at 2 GB a run, 4 on a host with 8 GB to grant, 12 on one with 24\.

```
[pipelines.gharchive]
memory_gb = "auto"       # learned from its runs
max_runs = "auto"
fill_max_runs = "auto"   # from 0.24.0
```

It needs a request to weigh, declared or learned. Until it has one, it starts one run at a time, and its queued runs say so. On EC2, Kubernetes and remote workers, where there is no host's room to weigh, the ceiling is the limit; a worker still admits what it takes against its own host. From 0.24.0, `fill_max_runs = "auto"` does the same for fill runs (migration 55 records it as `orca.pipelines.fill_max_runs_auto`). An older morca reads `"auto"` as its ceiling.

## Requests and grants

`cpus` and `memory_gb` are a request: the least a run needs to start and the most it can use (a single number is both). On the local runner, for control-placed runs and on remote workers, a run starts only if its minimum fits what the host can grant, less what the runs going there were granted. The host's grant is its CPUs and memory (its cgroup's limits if set, or `ORCA_HOST_CPUS` and `ORCA_HOST_MEMORY_MB`) less `ORCA_RESERVE_CPUS` (1) and `ORCA_RESERVE_MEMORY_MB` (1536); a worker's reserve is 0 and 512\. A run that starts gets up to its maximum of what's left, fixed for that start, in `ORCA_GRANT_CPUS` and `ORCA_GRANT_MEMORY_MB`.

- A pipeline without a request runs as before, unchecked and not counted against the others, so on a shared host give every pipeline one.
- On EC2 and Kubernetes the request is kept but not used yet.
- Where morca can make cgroups, a run's grant is its memory limit, and the kernel kills it `ORCA_RUN_MEMORY_MARGIN` (25) percent above that. Its retry and later runs then ask for `ORCA_RUN_MEMORY_GROWTH` (1.5) times as much, up to `memory_gb`'s maximum.
- `"auto"` takes the request from the pipeline's last 20 successful measured runs: memory at their 95th percentile plus 15%, CPUs at the 90th percentile of peak cores. With fewer than 3 such runs there is no request yet. `"auto..24"` caps it at 24.
- An `env` value `"auto"` lets the run size itself from its grant (`orca.auto_count`). `DISTA_WORKER_SLOTS = "auto"` becomes the granted CPUs over the run's local Dista workers, or 0 (one per core) without a grant.

[Scheduling and running](https://morca.dev/docs/run#resources) explains this in prose, and [orca-toml.md](https://github.com/morca-dev/morca/blob/main/docs/orca-toml.md#requests-and-grants) has every rule.

## Memory pressure

On top of `max_runs`, `fill_max_runs` and grants, no new run starts on the local runner, the control host or a worker while Linux's pressure stall information reads `some avg10` over `ORCA_ADMIT_MEMORY_PRESSURE` (10%; 0 turns it off). Runs already going go on. The hold ends under half the limit, or after 30 s at most at the limit. One that lasts `ORCA_ADMIT_PRESSURE_GRACE_SECONDS` (600) with none of morca's runs going raises the `memory-pressure` alert and lets runs start one at a time. Without PSI, as on macOS, nothing is held. These are environment variables, not orca.toml keys.

## env and secrets

- A pipeline's `env` goes only to its own runs: their process environment on the local runner, the control host and a worker, each pod's `env` on Kubernetes, and each instance's script and containers on EC2.
- `ORCA_RUN_ENV` still copies the scheduler's named variables to every run; a pipeline's `env` wins for a name it sets.
- On EC2 the env is part of the warm pool's key, so changing it launches new instances.
- Stack settings (database, warehouse, logs, runner, budget) stay in the scheduler's environment file, not in `env`.
- `secrets` are read with `ORCA_SECRET_FETCH` when it is set (`<command> <id>` through `sh -c`), else from Secrets Manager. IDs always start with `orca/`.

## Fills

With `fills = "all"`, on every tick and after the pipeline's other runs have started, the scheduler queues fill runs over its range: its first partition through the last one that ended at least `fill_lag` ago. Each fill run takes `keys_per_run` partitions in `fill_order`, with trigger `fill`. A fill never runs a partition twice: one done in any of the pipeline's code, in a live run, or ever queued as a fill is taken. Fill runs don't count against `max_runs`, never start while other runs of the pipeline are queued, and on a worker pool are left for the pool's workers to claim, with `fill_max_runs` across the whole pool.

The cost estimate fails closed: an hour of the head plus the most workers at on-demand prices, times 1.2, against today's spend on fill runs' instances. Runs on the control host or a local process cost nothing and need no budget. Keyed pipelines, sensors and pipelines with no first partition can't fill, and `orca register` refuses `fills = "all"` for them. The pipeline page's Fill line and `fill` in its JSON say why a fill is idle:

| Idle because | Meaning |
| --- | --- |
| complete | every partition of the range is done, or the last ones are running |
| paused | the pipeline is paused, by hand or by the watchdog, whose rule the reason names |
| outside its window | the UTC hour is outside fill\_window; a fill run already going goes on |
| no budget | the runs cost money (EC2 or Kubernetes) and fill\_budget\_per\_day isn't set |
| price unknown | orca.fleet's price table can't price the run, or it runs on Kubernetes; the estimate fails closed |
| budget | today's estimated spend plus the next run's would go over fill\_budget\_per\_day; it starts again at 00:00 UTC |
| waiting | other runs of the pipeline are queued, and they start first |
| at fill\_max\_runs | as many fill runs are going as fill\_max\_runs allows |

## \[pools.<name>\]

A `[pools.<name>]` table makes a pool of remote workers that orca grows and shrinks itself. Pipelines run on it with `placement = "worker:<name>"`, as on workers started by hand. `orca register` records the file's pools in `orca.pools`; a pool the file no longer has is deleted there, and its machines go once idle.

```
[pools.lab]
provider = "hetzner"
machines = [
  { type = "cax31", wait = 600 },   # prefer this; keep trying it for 10 min
  { type = "cax21", wait = 600 },
  { type = "cpx32" },               # the last: no wait
]
locations = ["hel1", "nbg1", "fsn1"]
max_machines = 3
idle_seconds = 900
```

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| provider | "hetzner" or "ec2" | required | Where the machines are made. |
| machines | list of { type, wait } | required | Machine types in order of preference. wait (0 to 86400) is how many seconds morca keeps trying a type before the next; every entry but the last needs one. |
| locations | list of strings | none (required on Hetzner) | Where each type is tried, in order: Hetzner locations, or EC2 subnet IDs (none: the default subnet or ORCA\_EC2\_SUBNET). |
| max\_machines | integer 1 to 100 | 1 | The most machines the pool has at once. |
| idle\_seconds | integer 10 to 86400 | 900 | How long a machine may sit idle, with nothing queued for the pool, before it's removed. |

- **Grow.** While runs of the pool's pipelines have waited 20 s that a worker could start, no machine is still booting and the pool has fewer than `max_machines`, the leading scheduler makes one. It tries the current type in each location in turn for its `wait`, then the next type, and skips a type whose architecture the worker image lacks. From 0.24.0, a type the provider refuses for the project's quota (Hetzner's `resource_limit_exceeded`) moves it on to the next usable type at once.
- **Boot.** A machine holds only a worker token of its own and asks `/api/worker/boot` for the image, its login and the bucket's variables. From 0.24.0 machines are named `orca-pool-<pool>-<n>`.
- **Shrink.** A machine idle for `idle_seconds` with nothing waiting is removed, its token revoked first. So is one whose worker never shows up (15 min) or goes silent (5 min), and one with the pool's labels that morca doesn't know.
- A running machine is never replaced by a type higher in the list: the order only matters when morca adds one.

What the scheduler and the UI need in their environment for pools:

| Variable | What it sets |
| --- | --- |
| ORCA\_HCLOUD\_TOKEN, ORCA\_HCLOUD\_TOKEN\_SECRET | Hetzner's API token for the scheduler, or the secret that holds it. |
| ORCA\_HCLOUD\_FIREWALL, ORCA\_HCLOUD\_SSH\_KEYS, ORCA\_HCLOUD\_IMAGE | the firewall and SSH keys (by name) each Hetzner machine gets, and its OS image (ubuntu-24.04). |
| ORCA\_HCLOUD\_NETWORK, ORCA\_HCLOUD\_NETWORK\_ZONE, ORCA\_HCLOUD\_NETWORK\_RANGE | the private network every Hetzner machine joins (orca-<stack id> in eu-central over 10.77.0.0/16 unless set; none for none). |
| AWS\_REGION and credentials, or an instance role | EC2's API, for provider = "ec2". |
| ORCA\_POOL\_EC2\_SECURITY\_GROUP, ORCA\_POOL\_EC2\_PROFILE | each EC2 machine's security group and instance profile, else ORCA\_EC2\_SECURITY\_GROUP and ORCA\_EC2\_PROFILE. |
| ORCA\_POOL\_EC2\_AMI\_ARM64, ORCA\_POOL\_EC2\_AMI\_AMD64 | the EC2 image per architecture, else Ubuntu 24.04 from Canonical's public SSM parameter. |
| ORCA\_WORKER\_IMAGE | the image pool machines run, the server's own (scheduler and UI). |
| ORCA\_UI\_URL, or ORCA\_POOL\_REMOTE | where pool machines reach the UI. |
| ORCA\_REGISTRY\_USER, ORCA\_REGISTRY\_TOKEN\_SECRET | the login for a private image: the UI hands it to a booting machine, and the scheduler reads the image's architectures with it. |
| ORCA\_POOL\_SCALE\_SECONDS (15), ORCA\_POOL\_GROW\_AFTER (20), ORCA\_POOL\_TRY\_SECONDS (60) | how often the autoscaler looks, how long runs wait before it grows, and the time between tries. |
| ORCA\_POOL\_BOOT\_SECONDS (900), ORCA\_POOL\_LOST\_SECONDS (300), ORCA\_POOL\_IMAGE\_SECONDS (600) | how long a machine has to join, how long a worker may go silent, and how often the image's architectures are read. |

[Pools and autoscaling](https://morca.dev/docs/pools) covers running them.

## \[apps.<name>\]

From 0.26.0, an app is a long-running binary of the project's image, `/opt/morca/bin/<name>`, that runs beside the UI: the census console is one. Its table says what the stack gives it, as a pipeline's table says how its runs run. Which apps a stack runs is the stack's choice ([Apps](https://morca.dev/docs/deploy#apps) on the deploy page).

```
[apps.console]
port = 8080                                   # $PORT
check = "/healthz"                            # 200 once it's ready
env = { CONSOLE_SEMANTIC = "1" }
lake = true                                   # the warehouse and the stack's S3 keys
secrets = { POSTHOG_KEY = "orca/posthog-key" } # an ID, never the value
memory_gb = 1
```

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| port | integer 1 to 65535 | none: the stack's APPS must give one | Where the app listens, passed as PORT; the tunnel's route points there. |
| check | string, a path | / | Answers 200 once the app is ready. A new app serves only then, and the old one keeps serving until it does. |
| env | table of NAME = "value" | none | Its variables, one line each. Not PORT, the lake's names, or a name that looks like a secret's (PASSWORD, SECRET, TOKEN). |
| lake | boolean | false | true gives it the warehouse settings (MORCA\_WAREHOUSE, MORCA\_PUBLIC\_WAREHOUSE, AWS\_REGION, AWS\_ENDPOINT\_URL\_S3) and the stack's S3 keys. |
| secrets | table of NAME = "orca/<id>" | none | Each secret's value, passed as NAME. The file holds IDs only, never values. |
| memory\_gb | number over 0 | none: no limit | Its memory limit (docker --memory). |

- **Nothing it doesn't ask for.** An app never gets morca's database, and gets the lake only with `lake = true`.
- **Names.** Lower-case letters, digits, `_` and `-`, and not one of the stack's own containers (`scheduler`, `ui`, `sampler`, `cloudflared`, `postgres`).
- **Overlays.** A stack's overlay can change an app the file declares, but can't add one.

`morca config apps [<file>] [--overlay F]` prints the apps one fact per line, as the stack's script reads them, with secrets by ID:

```
$ morca config apps
console port 8080
console check /healthz
console lake true
console memory_mb 1024
console env CONSOLE_SEMANTIC=1
console secret POSTHOG_KEY orca/posthog-key
```

## What is refused

Each refusal names the file and line, and nothing is registered:

```
orca.toml:7: [pipelines.gharchive] has no key workrs (it has placement, head_type, ...)
orca.toml:3: [pipelines.toy] workers: not a count or a range MIN..MAX: 4..2
```

- Unknown keys and tables, and a value of the wrong type.
- A `workers`, `cpus` or `memory_gb` range like `"4..2"`; `cpus = 0`, `cpus = 1.5` or `memory_gb = 0`; `auto` anywhere but alone or as a range's lower end, or `"auto.."` without its maximum.
- A secret ID outside `orca/`.
- A `fill_window` like `"6..6"` or `"1..30"`, a negative `fill_budget_per_day`, or `fill_max_runs = 0`.
- A `fill_lag` without a unit, with an unknown one (`"2w"`), or over `31d`.
- `fills = "all"` for a keyed pipeline, a sensor, or one with no first partition.
- An `env` name that isn't a valid variable name, that looks like a secret (`*TOKEN*`, `*SECRET*`, `*PASSWORD*`, `*DATABASE_URL*`), or that morca sets itself: `ORCA_DATABASE_URL`, `ORCA_IMAGE`, `ORCA_SECRETS`, `ORCA_WORKERS`, `DISTA_ROLE`, `DISTA_HEAD`, `DISTA_ADVERTISE`, `ORCA_GRANT_CPUS`, `ORCA_GRANT_MEMORY_MB`, `ORCA_MEMORY_PRESSURE_FILE`.
- A pool without `provider` or `machines`, a provider other than `hetzner` or `ec2`, a machine entry with a key other than `type` and `wait`, a type that isn't 1 to 64 of `a-z`, `0-9`, `.`, `-` and `_`, a type listed twice, a missing `wait` on any entry but the last, a Hetzner pool without `locations`, or `max_machines` or `idle_seconds` out of range.

## How orca register reads it

| How | File read |
| --- | --- |
| orca register --config F | F |
| ORCA\_CONFIG=F | F |
| neither | ./orca.toml, if it exists |
| \--no-config | none |

- **A pipeline the file names** is registered with exactly what the file and flags say. A key removed from the file goes back to its default, and a flag applies to that registration only.
- **A pipeline the file doesn't name** keeps what was registered for a flag left out, and `orca register` warns that it isn't in the file.
- **Without paths**, `orca register` registers every `[pipelines.<name>]` as `<dir>/<name>`, where the directory is `ORCA_PIPELINES_DIR` or `pipelines/` next to the file. A missing binary is an error, and nothing is registered. A project's image sets `ORCA_CONFIG`, `ORCA_PIPELINES_DIR`, `ORCA_COMMIT` and `ORCA_SOURCE`, so `orca register` alone registers the whole file.
- **`orca aws register`** takes the same `--config` and `--no-config`, checks the file locally, and sends it whole to the control instance with its git commit.
- **What's recorded**: `orca.pipelines.config` keeps the file's name, SHA-256, git commit and whether it had uncommitted changes, the flags that overrode it, and the source of every value.

## orca config show, check and export

```
$ orca config show gharchive
gharchive  registered from orca.toml @ 3f2a9c1d0b7e, sha256 b0e8af31ea3e
  overrides: --workers 0..8
  placement               ec2         orca.toml [defaults]
  worker_type             c7g.xlarge  orca.toml [pipelines.gharchive]
  workers                 0..8        flag --workers
  keys_per_run            1           code default (the pipeline's keys_per_run)
  env.DISTA_WORKER_SLOTS  4           orca.toml [pipelines.gharchive]
```

- `orca config show [pipeline]` prints each registered value with its source: `code default`, `orca.toml`, `flag` or `environment`, and a learned request after the settings. `--json` prints one object, or `{"pipelines": [...]}` without a name. The pipeline page's Configuration card and `config` in `/api/pipelines/<name>` show the same.
- `orca config check [file] [--secrets]` refuses what `orca register` would, with no database, for a project's CI. It warns about an instance type missing from morca's price table, and with `--secrets` reads each secret without printing it. It exits 1 on any problem.
- `orca config export [--out -] [--force]` writes an `orca.toml` with a table per registered pipeline, holding what is registered beyond the defaults, so registering from it gives the same registration back. It writes no `[defaults]`. Use it to move from register flags to a file.

[morca](https://morca.dev/)

Data pipelines in Mojo. Early, and built in the open.

### Project

[GitHub organisation](https://github.com/morca-dev) [Issues](https://github.com/morca-dev/morca/issues) [morca](https://github.com/morca-dev/morca) [dista](https://github.com/morca-dev/dista) [census](https://github.com/morca-dev/census)

### Built on

[magmalake.dev](https://magmalake.dev) [The Iceberg, Parquet and codec libraries](https://magmalake.dev/tins) [mojoshelf.org](https://mojoshelf.org) [The Mojo language](https://www.modular.com/mojo)

### Standards

[Apache Iceberg](https://iceberg.apache.org) [Apache Parquet](https://parquet.apache.org) [PostgreSQL](https://www.postgresql.org)

Copyright 2026 Marius Seritan. Every repository is licensed [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).

Apache Iceberg and Parquet are trademarks of the Apache Software Foundation. Mojo is a trademark of Modular Inc. This project is not affiliated with or endorsed by the ASF or Modular.
