Jobs

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 with 400.
  • A multipart/form-data upload 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 ./out

The 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.

EndpointReturns
runs/<run>/statusThe run's state as plain text
runs/<run>/statsRun metadata — timings, trigger, who started it
runs/<run>/logThe run's timestamped log
runs/<run>/outputThe 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:

FieldDescription
nameThe run id
shortnameThe run's auto-generated shortname
statusSame value as the status endpoint
origin / origin_shortnameThe job the run belongs to
started_on / completed_onUTC timestamps; completed_on is null while running
durationSeconds, two decimals; null while running
triggerapi, email or schedule
sourceThe client that triggered it (cli, webpage, scheduler, …)
has_outputWhether output has anything to download
created_byUsername of whoever triggered the run
childrenNested 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:

EndpointReturns
GET .../jobs/<job>/logThe latest run's log — same formats as a run's log
GET .../jobs/<job>/stats/runsThe 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 / /output contract.
  • Schedule — trigger runs on a cron schedule.
  • Config — every job configuration key.