---
myst:
html_meta:
description: "Upload many files as one ingest job, wait for its files to ingest and find failed jobs."
---
# Ingest jobs
{.lead}
An ingest job tracks a batch of files as one unit, with one status and one entry on the [Ingestion page](https://app.gov.nominal.io/ingestion).
You get one from {py:class}`~nominal.experimental.ingest.IngestBuilder` or from a [file extractor](/guides/extractors/overview.md).
To upload a single file, use the dataset's own methods, as in [Datasets](/guides/datasets/overview.md).
```{include} /guides/_snippets/connect-the-client.md
```
## Submit a batch
`IngestBuilder` uploads several files in parallel, then starts one job for them.
It's experimental, so the API may change between releases.
```python
from datetime import datetime, timezone
from nominal.experimental.ingest import IngestBuilder
dataset = client.get_dataset("")
job = (
IngestBuilder(client, dataset)
.add_tabular_data(
"flight_data.csv",
timestamp_column="source_time",
timestamp_type="iso_8601",
units={"altitude": "m", "airspeed": "m/s"},
)
.add_mcap("recording.mcap", exclude_topics=["/diagnostics"])
.add_video("nose_camera.mp4", "nose_camera", start=datetime(2026, 9, 10, 14, 38, 19, tzinfo=timezone.utc))
.add_tags({"test_event": "ft-114"}) # applied to every file
.submit()
)
```
The `add_*` methods match the [dataset upload methods](/guides/datasets/overview.md#file-formats).
`add_tabular_data` also takes `units`, `channel_prefix` and `channel_name_overrides`, to rename vendor columns as they ingest.
`submit` returns once the job starts, not when its files finish.
A builder submits once. Make a new one for the next batch.
- If an upload fails, `submit` raises `NominalIngestUploadFailed` and starts no job.
`submit(allow_partial=True)` starts the job with the files that uploaded and logs the rest.
- `submit(runs_to_expand=[""])` widens those runs to cover the new data.
- Files in one job share a single `nominal_ingest` tag. To tell them apart later, pass your own `tags` to each `add_*` call.
If `submit` raises after sending its request, the job may still have started.
[Search for it](#find-jobs) before you retry, so you don't ingest the files twice.
## Wait for the files
{py:meth}`~nominal.core.IngestionJob.as_files_ingested` yields each file as it finishes, including files the job produces while you wait:
```python
from datetime import timedelta
from nominal.core.dataset_file import IngestStatus
for file in job.as_files_ingested(timeout=timedelta(minutes=30)):
if file.ingest_status is IngestStatus.SUCCESS:
print(file.name, "ingested")
else:
print(file.name, file.ingest_status.name, file.get_ingest_error())
```
- A completed job can still have failed files, so check each file's status.
- A failed job raises `NominalIngestFailed`, after yielding the files that finished.
- A timeout raises `NominalIngestTimeout` but doesn't stop the job. {py:meth}`~nominal.core.IngestionJob.cancel` does, without removing data that already ingested.
To wait on files you already have, use {py:func}`~nominal.core.wait_for_files_to_ingest`.
It returns `(done, not_done)` and doesn't see files the job adds later.
## Find jobs
{py:meth}`~nominal.core.NominalClient.search_ingestion_jobs` filters by dataset, creator, status, text and start time:
```python
from datetime import datetime, timedelta, timezone
from nominal.core import IngestionJobStatus
failed = client.search_ingestion_jobs(
statuses=[IngestionJobStatus.FAILED],
start_time_after=datetime.now(timezone.utc) - timedelta(days=1),
)
for job in failed:
print(job.ingest_type, job.origin_files, job.nominal_url)
```
A job's `status` doesn't change until you call `job.refresh()`.