Runs
Every time a job is triggered, novem records a run. Each run keeps its own log, output, stats and status, reachable under the job's runs directory.
AI assisted, human approved — novem uses AI to review and keep our documentation up to date.
A run is one execution of a job's chain,
whether you triggered it by hand, a schedule
fired, or an inbound e-mail kicked it off. Runs live under the job's runs
directory:
daily_report
├── data => POST here to trigger a run
├── log => Latest run's log (shortcut)
├── stats
│ └── runs => Run history (time, trigger, duration, status)
└── runs
└── <run> => One entry per run
├── log => The run's log
├── output => The run's result file(s)
├── stats => Run metadata (timings, trigger, …)
└── status => processing / success / failed
Trigger a run by posting to the job's data endpoint (this is what the CLI's
-R flag does). What you send becomes the run's input, mounted at /input
inside the first chain step:
- A JSON body (
Content-Type: application/json) is stored as/input/input.json. The body must be a JSON document; an empty or non-JSON body is rejected with400. - A
multipart/form-dataupload stores each file as/input/<filename>, original names preserved.
# trigger a run (input files each prefixed with @)
novem -j daily_report -R
novem -j daily_report -R @data.csv
# save the run's output to disk
novem -j daily_report -R -o ./outThe request stays open while the chain executes, and the response body is the
run's result: a single output file is returned as-is, several are bundled
into one .zip. See how your code runs for the
/input / /output contract.
Triggering requires write access to the job; reading runs requires read access.
GET /v1/code/jobs/<job>/runs returns the 30 most recent runs as a JSON directory listing, newest
first. Each entry's name is the run id, <YYYYMMDD>-<HHMMSS>-<request id>,
with created_on set to when the run started and last_modified to when
it completed (or started, while still running).
[
{
"name": "20260613-081502-f3a9c2d4...",
"uri": "/v1/code/jobs/daily_report/runs/20260613-081502-f3a9c2d4...",
"type": "dir",
"permissions": ["r"],
"actions": ["OPTIONS", "GET"],
"created_on": "2026-06-13T08:15:02Z",
"last_modified": "2026-06-13T08:15:31Z"
}
]
GET .../runs/<run> lists the four per-run files described below.
| Endpoint | Returns |
|---|---|
runs/<run>/status | The run's state as plain text |
runs/<run>/stats | Run metadata — timings, trigger, who started it |
runs/<run>/log | The run's timestamped log |
runs/<run>/output | The run's result file(s) |
A single plain-text value: processing while the chain executes, then
success or failed. A run that never started because a permission check
blocked it shows rejected.
Run metadata. Plain text key: value pairs by default; send
Accept: application/json for a JSON object:
| Field | Description |
|---|---|
name | The run id |
shortname | The run's auto-generated shortname |
status | Same value as the status endpoint |
origin / origin_shortname | The job the run belongs to |
started_on / completed_on | UTC timestamps; completed_on is null while running |
duration | Seconds, two decimals; null while running |
trigger | api, email or schedule |
source | The client that triggered it (cli, webpage, scheduler, …) |
has_output | Whether output has anything to download |
created_by | Username of whoever triggered the run |
children | Nested runs for any composed jobs, [] when none |
The run's log entries in chronological order. Plain text by default. With
Accept: application/json you get an array of
{ "log_time", "severity", "message" } objects with UTC ISO timestamps. A
run with no log entries returns an empty 200.
Downloads the run's result with its original filename and content type, the
same payload the triggering request received. A run that produced no output
returns 204 No Content.
When a chain references another job, each referenced job invocation gets its own run, nested under the run that triggered it. The result is a run tree: the top-level run owns its steps, and every composed job hangs beneath it as a child run with its own status, timings and duration.
The stats endpoint carries this tree in its children array. Each entry
names the child run, the job it came from, the step it fulfils, and its own
timing, so cost rolls up from the parts:
{
"name": "20260617-081502-f3a9c2d4...",
"status": "success",
"duration": 42.5,
"children": [
{
"name": "20260617-081507-9b1e...",
"shortname": "kPq7T",
"job": "nightly_rollup",
"job_shortname": "VA0PG",
"parent_step": "rollup",
"status": "success",
"started_on": "2026-06-17T08:15:07Z",
"completed_on": "2026-06-17T08:15:29Z",
"duration": 22.0
}
]
}
Child runs nest under their parent and are not listed as standalone runs of the referenced job: the runs listing and history for a job show only its top-level runs. Whoever can read the top-level run can read its descendants.
Run logs and output are retained for 30 days. After that, log and
output answer 410 Gone; the run listing, stats and status remain
available indefinitely.
Two conveniences live directly on the job:
| Endpoint | Returns |
|---|---|
GET .../jobs/<job>/log | The latest run's log — same formats as a run's log |
GET .../jobs/<job>/stats/runs | The full run history: one row per run with time, trigger, duration and status (plain-text table, or JSON with Accept: application/json) |
stats/runs is not capped at 30 entries, so it's the place to look when the
runs listing has rotated past what you're after.
Note: Runs are never public. Even when a job is shared with public,
its runs, logs and output are only readable by authenticated users with
read access to the job: the owner and explicit share grantees.
- Jobs overview — the
/input//outputcontract. - Schedule — trigger runs on a cron schedule.
- Config — every job configuration key.