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 / canceled
POST "canceled" here to stop the run
Triggering a run
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.
Listing runs
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.
Per-run endpoints
| Endpoint | Returns |
|---|---|
runs/<run>/status | The run's state as plain text; POST here to stop it |
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) |
status
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. A run you asked to stop reports canceling
while it winds down, then canceled — see stopping a run.
Stopping a run
POST the desired end state to a run's status to stop it:
curl -X POST -H "Authorization: Bearer $NOVEM_TOKEN" \
-H "Content-Type: text/plain" \
--data 'canceled' \
https://api.novem.io/v1/code/jobs/daily_report/runs/<run>/statusThis records an intent, not an outcome. novem tears the run down — killing the container or stopping the chain — but a run that was already finishing may get there first, so the end state is whichever actually happened:
processing ──POST canceled──▶ canceling ──▶ canceled (stopped in time)
├─▶ success (it finished first)
└─▶ failed (it broke first)
The run reports canceling in the meantime, and the request answers 202.
Asking again while a cancel is pending is a no-op (200); asking for a run
that already finished answers 409 with its final status. Stopping requires
the same execute access as triggering a run — if you can start it, you can
stop it.
A stopped run keeps whatever log it produced, is not counted as a failure (so it never contributes to a job being auto-paused), and produces no output.
The original triggering request stops waiting as soon as the run is torn down:
the blocked HTTP call to data answers 409, and a mail-triggered run gets
the error reply, which names the cancel. To find out why a run ended, read its
status and log — the trigger's own error body is deliberately generic.
Note: Runs of a composed chain are stopped
through their top-level run. Cancelling a nested child run alone would leave
the parent waiting for output that will never arrive, so it answers 409.
stats
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 |
log
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.
output
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.
Composed jobs: the run tree
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.
Retention
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.
Job-level shortcuts
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.
Next steps
- Jobs overview — the
/input//outputcontract. - Schedule — trigger runs on a cron schedule.
- Config — every job configuration key.