Jobs
A job is a single execution of a gear. Every call to gear.run() (utility gears) or every analysis gear submission produces a job under the hood. The SDK lets you find, inspect, monitor, cancel, retry, and bulk-submit jobs through the fw.jobs finder and a handful of dedicated methods.
For how to launch a job, see Gears. This page picks up after submission.
Job lifecycle
Every job moves through a small state machine. Three states — complete, failed, and cancelled — are terminal: a job in any of these states will not transition again.
flowchart LR
pending --> running
running --> complete
running --> failed
pending --> cancelled
running --> cancelled pending— accepted by the queue, not yet picked up by a worker.running— a worker has started executing the gear.complete— the gear exited successfully.failed— the gear exited non-zero or the worker reported a failure.cancelled— the job was cancelled by a user or by the scheduler.
Job fields
The most-used fields on a Job object:
| Field | Description |
|---|---|
id | The job's unique ID |
gear_id | ID of the gear that produced this job |
gear_info.name | Name of the gear (use this for filter queries) |
state | Current lifecycle state |
created / modified | Timestamps |
inputs | Inputs the gear ran against, as FileReferences |
config | Effective config (defaults merged with overrides) |
destination | Where outputs land — a container or new analysis |
tags | User-supplied labels, useful for grouping batches |
failure_reason | Populated only on terminal failed jobs |
attempt | Retry count; the first run is attempt 1 |
retried | Set to True once this job has been retried; None otherwise |
previous_job_id | ID of the original job this was retried from; None for the original |
Logs do not live on the Job object — fetch them separately with fw.get_job_logs(job_id). See Reading job logs.
Finding jobs
fw.jobs is a Finder, so the standard find / find_first / iter / iter_find methods all apply.
The most useful filter keys:
| Key | Example |
|---|---|
state | state=failed |
gear_info.name | gear_info.name=dicom-qc |
gear_id | gear_id=000123456789abcdefABCDEF |
tags | tags=my-batch-tag |
origin.id | origin.id=<user_id> |
parents.project | parents.project=000123456789abcdefABCDEF |
parents.group | parents.group=scientific-solutions |
Filter by gear and state
jobs = list(fw.jobs.iter_find(f"gear_info.name={gear_name},state=cancelled"))
for job in jobs:
print(f"{job.id} ({job.state})")
Filter by gear and submission date
Use the created field with a comparison operator to scope a query to a time window. Dates are interpreted as UTC and may be supplied as YYYY-MM-DD or a full ISO-8601 timestamp.
gear_name = "dicom-qc"
since = "2026-05-26" # midnight UTC — anything submitted on or after
jobs = list(fw.jobs.iter_find(f"gear_info.name={gear_name},created>{since}"))
for job in jobs:
print(f"{job.id} — submitted {job.created}")
Get a job by ID
job = fw.get_job(job_id)
print(f"{job.id} — state: {job.state}, gear: {job.gear_info.name}")
Monitoring a job
Polling job state
Job execution is asynchronous. To wait for a job to finish, poll its state until it reaches a terminal value (complete, failed, or cancelled).
import time
TERMINAL = ("complete", "failed", "cancelled")
job = fw.get_job(job_id)
while job.state not in TERMINAL:
time.sleep(5)
job = fw.get_job(job_id)
print(f"Job finished in state: {job.state}")
Tip
Pick a poll interval that matches the gear's expected runtime. Quick utility gears (file conversion, QC) can tolerate a 2–5 second interval; long-running analysis gears are better served by a 1-5 minute interval to avoid hammering the API.
Reading job logs
fw.get_job_logs(job_id) returns a JobLogRecord whose logs attribute is a list of log entries. Each entry has a msg field with the line of text the worker emitted.
record = fw.get_job_logs(job_id)
for entry in record.logs:
print(entry.msg, end="")
Logs may be empty for jobs that have not yet started running. Logs continue to be available after the job reaches a terminal state.
Cancel and retry
Cancel
Cancel transitions a job to the terminal cancelled state. It is only valid from pending or running — once a job is in any terminal state, cancel is a silent no-op.
Warning
A cancel against a terminal job (complete, failed, already cancelled) does not raise — the request returns successfully and the job's state does not change. Check job.state first if you need to know whether the cancel took effect.
Retry
Retry is only valid for a job in the failed state. It creates a new job with the same gear, inputs, and config; the original job stays in the failed state for provenance. The new job's ID is the return value.
Note
Retry is useful for issues with timeouts, or when a job operates on an entire session (or container), and the container's contents have changed. Retries will generally not help when an algorithm fails on a specific file. Even if the file is replaced, retry will run the gear with the original file. To run the gear with the new file version, a fresh run of the gear is necessary.
failed_job_id = "000123456789abcdefABCDEF"
new_job_id = fw.retry_job(failed_job_id)
print(f"Retry queued as job {new_job_id}")
Pass ignore_state=True to retry a job that is not in the failed state — useful when you need to re-run a complete job against new data.
Use the retried and previous_job_id fields to trace the relationship between an original job and its retry:
job_id = "000123456789abcdefABCDEF"
job = fw.get_job(job_id)
if job.state == "failed" and job.retried is not None:
retry_job = fw.jobs.find_first(f"previous_job_id='{job.id}'")
print(f"Retried as job {retry_job.id}")
Batch jobs
To launch the same gear against many containers in one submission, use gear.propose_batch(). This is preferred over hand-rolling a gear.run() loop because all jobs share a single batch ID, which makes them findable and cancellable as a unit.
proposal = gear.propose_batch(
[acq_a, acq_b],
config={"threshold": 0.7},
)
jobs = proposal.run()
print(f"Submitted {len(jobs)} jobs in batch {proposal.id}")
The BatchProposal returned by propose_batch() is not yet running — call .run() to enqueue the jobs and .cancel() to cancel the entire batch.
Batch proposal will try to match files to the gear's inputs based on file types. If the gear finds more than one file of the suggested type present in the container, it will not be able to successfully batch that particular job. Because of this, this is most useful for gears with single (or few) inputs, where the containers generally only have one of the target file type (for example, dcm2niix batch launched on acquisitions: dcm2niix only requires one input dicom, and acquisitions typically only have one dicom file).
For batches where each container needs different config or inputs, OR where the inputs are too complex to be reliably populated simply though type suggestions, fall back to a manual loop. Tag the jobs so you can find them as a group:
job_ids = []
for container, threshold in [(acq_a, 0.3), (acq_b, 0.7)]:
job_id = gear.run(
destination=container,
config={"threshold": threshold},
input_file=container.get_file("scan.nii.gz"),
tags=["my-batch-2026-05-21"],
)
job_ids.append(job_id)
Bulk cancel
Cancel many jobs at once by iterating a tag- or state-scoped finder query and calling modify_job on each result.
for job in fw.jobs.iter_find(f"tags={tag},state=pending"):
fw.modify_job(job.id, {"state": "cancelled"})
Note
For hundreds or thousands of jobs the per-job modify_job loop becomes the bottleneck. The Core API exposes a bulk endpoint (PUT /api/jobs/cancel/bulk) for this case, which is not surfaced on the SDK. Call it directly with fw-client when you need it.