Every enabled pipeline stage leaves one record on the job, so you can read what ran instead of trusting that it did.
You turned on a few pipeline stages – maybe pre-edit to clean the source and back-translation to catch drift – and a job came back completed_with_warnings. Which stage fell through? Did the human reviewer ever pick it up, or did it time out? What did the extra stages cost? A pipeline that runs several AI and human steps per locale is exactly the kind of thing that turns into a black box: output comes out, and you take it on faith that the stages in between did their work.
They don't ask you to take it on faith. Every enabled stage writes one record to the job's steps[] array – which stage, what status, what cost, when it started and finished. You read what each stage did; you don't trust that it ran. That is the whole job of this page.
New to the pipeline? Start with the Pipeline overview.
On this page
- Where the records live
- The steps array
- Mapping a stepId to a stage
- Step status: completed, failed, skipped
- How a step failure becomes a job warning
Where the records live#
The steps[] array is a field on the localization job. You don't fetch it separately – it arrives whenever you read the job:
GET /jobs/localization/:jobIdAuthenticate with your API key in the X-API-Key header. The full endpoint, the job-status values, and the outputData payload are covered on the single-job page; this page is about one field on that response – the per-stage trail – and what it tells you.
So the rule is simple: every job you read already carries its own audit log. A job with no pipeline enabled shows a single record, because core localization always runs. Turn on two optional stages and you get three records. The array grows with the pipeline, one entry per stage, in the order the stages ran.
The steps array#
Each entry in steps[] is the record of one stage. These are the fields you read to audit a run – which stage, with what outcome, at what cost, and when:
"steps": [
{
"stepId": "preEdit",
"type": "action",
"status": "completed",
"errorMessage": null,
"costUsd": 0.0012,
"externalRefType": null,
"externalRefId": null,
"externalRefUrl": null,
"createdAt": "2026-03-16T10:30:01.000Z",
"startedAt": "2026-03-16T10:30:01.000Z",
"completedAt": "2026-03-16T10:30:02.000Z"
},
{
"stepId": "localize",
"type": "action",
"status": "completed",
"errorMessage": null,
"costUsd": 0.0184,
"externalRefType": null,
"externalRefId": null,
"externalRefUrl": null,
"createdAt": "2026-03-16T10:30:02.000Z",
"startedAt": "2026-03-16T10:30:02.000Z",
"completedAt": "2026-03-16T10:30:05.000Z"
}
]| Field | Description |
|---|---|
stepId | Which pipeline stage this record is for. See the mapping table below. |
type | The kind of step. action for an automated stage. |
status | completed, failed, or skipped for this stage – independent of the job's status. |
errorMessage | Why this stage failed. null unless status is failed. |
costUsd | What this stage cost, in USD – a JSON number, or null. |
externalRefType, externalRefId, externalRefUrl | A pointer to an external record for stages that hand work to a third party – the human review stage. null for fully automated stages. |
createdAt, startedAt, completedAt | When the stage was created, picked up, and finished. |
Each record also carries an outputData field – the content that stage produced, in the same shape as the job's outputData. That payload is the translation, not the audit trail, so it's documented on the single-job page alongside the job-level outputData; the fields above are the ones you read to see what the pipeline did.
Two things these records give you that a single outputData blob can't. First, cost is itemized per stage, not only totaled per job – so when you enable back-translation and the bill moves, you can read exactly which stage moved it. Second, timing is per stage – a humanEdit record whose startedAt and completedAt are hours apart tells you the wait was the human, not the engine.
Read steps by stepId, not by position
The records appear in execution order, but don't index into the array by position – which stages ran depends on which you enabled, so position is not stable across jobs. Find a stage by its stepId (steps.find(s => s.stepId === "humanEdit")). The set of stepId values is fixed; the set present on any given job is whatever you turned on.
Mapping a stepId to a stage#
Each stepId names one pipeline stage. This is the lookup table from the value in the record to the stage it represents and the page that documents what that stage does:
stepId | Stage |
|---|---|
preEdit | Pre-localization AI edit |
localize | Core localization |
humanEdit | Post-localization human review |
postEdit | Post-localization AI review |
rephrase | Rephrase for natural copy |
backTranslation | Back-translation check |
localize is the one stepId that appears on every job, pipeline or not – it is the core translate step, and it always runs. The other five appear only when you've enabled that stage on the engine or in the request.
Step status: completed, failed, skipped#
Each step carries its own status, set independently of the job and of every other step. Three values:
Step status | Meaning |
|---|---|
completed | The stage ran and produced its output. |
failed | The stage ran and errored. errorMessage says why. |
skipped | The stage did not run to completion this time, even though it was enabled. |
completed and failed read the way you'd expect. skipped is the one worth pausing on, because it is not the same as "disabled". A stage you never turned on produces no record at all. A skipped record means the stage was enabled but was passed over for a reason the pipeline defines – the clearest case is human review: if the review window closes with no human response, that stage is marked skipped and the AI translation carries forward as final. The record is still there, so the skip is visible rather than silent.
A step status is not the job status
A failed step does not always mean a failed job. Most optional stages are non-critical: when one fails, its record reads failed, the engine carries the last good output forward, and the job still finishes with full outputData. The job status that results – completed_with_warnings – is explained on the single-job page. The step status tells you what happened to one stage; the job status tells you whether you got a translation.
How a step failure becomes a job warning#
When a non-critical stage fails, the failure shows up in two places at once, and they are two views of the same event. The steps[] record reads failed with an errorMessage – that's the detailed view. The same failure also surfaces as one entry in the job's top-level warnings array – that's the summary view your status-handling code branches on:
{
"id": "ljb_A1b2C3d4E5f6G7h8",
"status": "completed_with_warnings",
"outputData": { "title": "Hallo" },
"warnings": [
{ "step": "backTranslation", "message": "Back-translation check did not complete" }
],
"steps": [
{ "stepId": "localize", "type": "action", "status": "completed", "errorMessage": null, "costUsd": 0.0184, "completedAt": "2026-03-16T10:30:05.000Z" },
{ "stepId": "backTranslation", "type": "action", "status": "failed", "errorMessage": "Back-translation check did not complete", "costUsd": 0.0031, "completedAt": "2026-03-16T10:30:11.000Z" }
]
}Each warnings entry is { step, message }, where step is the same stepId you'd find in the failed record. So the two arrays line up: warnings is the short list of what went wrong, and steps[] is where you go for the detail behind each one. Read warnings to decide whether to flag the locale for a human; read the matching steps[] record when you want the errorMessage, the cost, and the timing behind it.
This is the mechanism behind completed_with_warnings: the core translation succeeded, so you have usable outputData, but at least one non-critical stage left a failed record and a matching warning. Treat the output as shippable and the warnings as a quality signal worth surfacing. Only a job status of failed means there is no translation to read – and that decision, with the full job-status table, lives on the single-job page.
Aggregate stage health is a separate surface
steps[] answers "what did the pipeline do on this job." When you want the trend across many jobs – how often pre-edit fails, how often back-translation corrects a translation – that's an aggregate question, and it's answered on the Reports page, not in a per-job response. Per-job records here; rollups there.
