Skip to content

CSV Import

CSV import lets you create or update many jobs in one pass, from a spreadsheet, instead of going through the job form one job at a time. It’s the same create/update path as the admin UI and the API — every imported row runs through the same validation and defaults, so an imported job can’t bypass a rule a hand-authored one follows.

In the admin UI, open the Jobs page and click Import CSV, next to New job.

Terminal window
curl https://<your-host>/api/jobs/import/template -o jobs-template.csv

The Import CSV modal links to the same download. The template’s header row covers every settable job field — name, scheduling, HTTP or script config, loop, retry, pooling, and so on — plus one filled-in example row to copy from. See Job Fields for what each column means and how it’s validated; the CSV columns map directly to those fields.

The capabilities_json column holds a script job’s volume declaration as JSON, for example {"volumes":[{"name":"reports","mode":"rw"}]}. Leave it blank for no declaration. An import row runs the same validation as the API: the volumes must exist, the mode cannot exceed a volume’s max_mode, and a row for an HTTP job with a non-empty value is reported as a per-row error. See Volumes. The column is part of the downloaded template.

Terminal window
curl -X POST https://<your-host>/api/jobs/import \
-F on_collision=merge \
-F merge_priority=source

The response streams as newline-delimited JSON — one row result per line, printed as soon as that row finishes — rather than a single JSON body, so curl prints each row’s outcome as it happens instead of all at once at the end.

A row is matched against existing jobs by group_name + name. No match is a plain create. A match is a collision, handled according to on_collision:

on_collision Effect
overwrite Replace the existing job’s fields with the CSV row’s.
skip Leave the existing job untouched.
merge Combine the existing job and the CSV row — see below.

merge also needs merge_priority, source (the CSV row wins) or target (the existing job wins). Priority only decides the outcome when both sides have a non-blank value for a field — if a field is blank on one side, the other side’s value is used regardless of priority. A blank cell never wipes out an existing value in a merge.

Just as editing a job in the UI can’t change its job_type after creation, an import row that would change an existing job’s type across a collision is not applied — it’s reported as a per-row error instead, and the rest of the batch continues.

Script jobs still need the script permission

Section titled “Script jobs still need the script permission”

A CSV row with job_type set to bash, javascript, typescript, or groovy needs the same jobs:script permission a script job created directly in the UI needs (see Script Jobs). A script row uploaded without that permission is reported as a per-row error — it doesn’t fail the rest of the import.

A group_name is limited to 255 bytes and 16 levels deep — the name is normalized first (each /-separated segment is trimmed of whitespace and blank segments are dropped), and the limits apply to the normalized name, so a/ /b is 2 levels. A row over either limit is always reported as a per-row error and never skipped, whatever the on_collision policy, with the message group_name must be at most 255 bytes or group_name must be at most 16 levels deep; the rest of the batch continues. An existing job whose group_name is over the limits cannot be repaired by import: the row’s own group_name is validated before the collision lookup, so an over-limit value is a row error. Fix it through the job’s edit form or a PUT update.

Nothing aborts the batch, and progress is live

Section titled “Nothing aborts the batch, and progress is live”

Every row in the file gets its own outcome — created, updated, skipped, or error. The response streams one result per row as it happens, rather than waiting for the whole file to finish, so the admin UI’s import screen fills in its results table live, row by row, while the upload is still running. One malformed or rejected row never blocks the rows around it — it just reports its own error and the rest of the batch keeps going.