Chains
Compose up to five task-type steps into a single survey, with optional rules to end the walk early.
A chain bundles 1 to 5 task-type steps into a single survey. Each annotator answers the steps in order, and you get aggregated results per step. Use chains when later questions depend on earlier ones: gate a quality rating behind a watchability check, follow a tag selection with a confidence rating, or run a comparison and ask annotators to back up their choice.
When to use it
- Quality gate before scoring. Ask "is this clip watchable?" first; rate realism only when the answer is yes.
- Tag, then rate. Pick which categories apply, then score how strongly each one is present.
- Comparison + follow-up. Choose A or B, then answer a rating about the winner.
- Branching surveys. End the chain early on a specific answer to skip irrelevant downstream steps.
If your survey is a single question, use one of the standalone task types instead. Chains add ordering and skip_if plumbing you don't need for a single-step job.
Job shape
A chain job replaces the standalone fields (instruction, task_type, response_options) with a top-level steps array:
{
"name": "video-quality-chain-v1",
"max_responses_per_datapoint": 10,
"steps": [
{
"task_type": "multiple_choice",
"instruction": "Is this clip watchable?",
"response_options": { "mode": "single" },
"skip_if": { "when_answer_equals": "no" }
},
{
"task_type": "rating",
"instruction": "Rate how realistic this clip looks.",
"response_options": { "scale": [1, 2, 3, 4, 5] }
}
],
"datapoints": [
{
"media_per_step": {
"0": {
"options": [
{ "id": "yes", "text": "Yes" },
{ "id": "no", "text": "No" }
]
},
"1": {
"subject": [
{ "url": "dp://e7d921ab3c45/sample.mp4", "type": "video" }
]
}
}
}
]
}Chain job request
| Field | Type | Description |
|---|---|---|
name* | string | Same idempotent unique-per-account name as a standalone job. |
steps* | array of step objects | 1 to 5 steps. Setting `steps` switches the job into chain mode; the standalone fields `instruction`, `dimensions`, `dimension_weights`, and `response_options` must be omitted. `task_type` may be left at its default (`"comparison"`) or set explicitly to `"chain"`; any other value is rejected. |
datapoints* | array | Each datapoint provides the media every step will operate on. Use `media_per_step` if steps need different shapes. |
max_responses_per_datapoint | integer (1–10000) | Target number of qualified walks per datapoint — walks that complete *every* step. A walk that ends early through a step's `skip_if` rule is screened out: it does not count toward this target and is never billed. Defaults to 10. |
serving_environment | "all" | "sandbox" | "prod" | Same meaning as standalone jobs. |
Step shape
step
| Field | Type | Description |
|---|---|---|
task_type* | "comparison" | "i2v_comparison" | "rating" | "ranking" | "multiple_choice" | The task type for this step. Steps can mix types (e.g. multiple_choice followed by rating). |
instruction* | string | Question shown to the annotator for this step. Cannot be empty. |
response_options | object | Task-type-specific options (rating `scale`, multiple_choice `mode`). Required when the underlying task type requires it. |
skip_if | object | Optional rule that ends the chain after this step when the annotator's submission matches. Forbidden on the last step. |
Skip rules
skip_if lives on a step and inspects the annotator's submission to that step. When the rule matches, every later step is skipped for that annotator. Three forms are accepted:
when_answer_equals
The simplest form: match an exact value. Use it for binary or single-pick gates.
{
"task_type": "multiple_choice",
"instruction": "Is this clip watchable?",
"response_options": { "mode": "single" },
"skip_if": { "when_answer_equals": "no" }
}For a rating step, the value is the numeric rating:
{
"task_type": "rating",
"instruction": "How clear was the audio?",
"response_options": { "scale": [1, 2, 3, 4, 5] },
"skip_if": { "when_answer_equals": 1 }
}when_answer_in
Match against a list of acceptable answers. Useful when several values mean "stop here":
{
"task_type": "multiple_choice",
"instruction": "Did you understand the speaker?",
"response_options": { "mode": "single" },
"skip_if": { "when_answer_in": ["no", "unsure"] }
}For multi-select multiple-choice, the rule fires if any picked option appears in the list:
{
"response_options": { "mode": "multi" },
"skip_if": { "when_answer_in": ["spam", "broken"] }
}predicate (escape hatch)
For comparisons the shorthand doesn't cover (numeric ranges, negation, compound logic), supply a JSON-Logic predicate directly. Supported operators: ==, !=, <, <=, >, >=, in, and, or, !, var. Predicates are nested up to 6 levels deep.
{
"task_type": "rating",
"instruction": "How realistic is this clip?",
"response_options": { "scale": [1, 2, 3, 4, 5] },
"skip_if": {
"predicate": {
"<": [{ "var": "rating" }, 3]
}
}
}Reads as: "skip the rest of the chain if the rating is below 3."
The submission field name (rating, choice, choices, ranking) is set by the step's task type. Predicates referencing unknown field paths are rejected at job-creation time.
Per-step media
By default, every step in a chain operates on the same media object you put on the datapoint. When steps need different shapes (for example, a multiple-choice option list followed by a rating subject), use media_per_step instead, keyed by string step index. The Job shape example above uses this form.
Keys must be the digit strings "0", "1", ... and must cover every step in the parent steps array (no missing or extra keys). media and media_per_step are mutually exclusive: set exactly one per datapoint.
How responses are counted
Only a qualified walk — one that completes every step — counts toward max_responses_per_datapoint and is billed. A walk that ends early through a skip_if rule finalizes as screened out: it is free (never billed), and it does not advance your target. Walks the annotator abandons partway through are likewise never billed and never counted.
Because some walks screen out or are abandoned, the platform keeps serving a datapoint until it has collected enough qualified walks (or it reaches a platform serving limit for that datapoint, or exhausts the eligible audience). You're billed only for qualified walks, up to max_responses_per_datapoint per datapoint.
GET /jobs/{id} returns a per-job chain_progress rollup so you can watch this fill:
{
"chain_progress": {
"target_responses": 200,
"completed_responses": 143,
"screened_out_responses": 27
}
}chain_progress
| Field | Type | Description |
|---|---|---|
target_responses | integer | Total qualified walks the job is aiming for: `max_responses_per_datapoint` × the number of datapoints still serving (failed or blocked datapoints are excluded). |
completed_responses | integer | Qualified walks collected so far, each datapoint capped at `max_responses_per_datapoint`. Each qualified walk bills one response per non-screener step, so the billed response count is this number times the number of those steps. |
screened_out_responses | integer | Walks that ended early through a `skip_if` rule. Reported for visibility only — never billed and never counted toward `target_responses`. |
Result shape
GET /jobs/{id}/results returns one entry per original datapoint, with a steps array carrying the per-step aggregation. Each step entry uses the same fields you'd see for a standalone job of that task type:
{
"datapoint_index": 0,
"context": null,
"steps": [
{
"step_index": 0,
"task_type": "multiple_choice",
"votes": { "yes": 10 },
"total_responses": 10,
"consensus": "yes",
"confidence": 1.0
},
{
"step_index": 1,
"task_type": "rating",
"mean": 3.9,
"median": 4,
"distribution": { "3": 3, "4": 5, "5": 2 },
"total_responses": 10
}
]
}step_index is zero-based and matches the position in your request's steps array. Results count only qualified walks, so every step reports the same total_responses — the number of walks that finished the entire chain. A gate step therefore surfaces only the answers that let a walk continue (here, all 10 qualified walks answered "yes"); the screened-out answers and the full step-by-step funnel are visible in the served view of GET /jobs/{id}/responses, not in results. Each step entry also carries the reliability-weighted verdict fields (weighted_votes, weighted_consensus, and their per-type analogues) beside their equal-vote counterparts, exactly as a standalone job of that task type does — see Task types.
Example
curl -X POST https://api.trydatapoint.com/data-labelling/v1/jobs \
-H "X-API-Key: $DATAPOINT_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "video-quality-chain-v1",
"max_responses_per_datapoint": 10,
"steps": [
{
"task_type": "multiple_choice",
"instruction": "Is this clip watchable?",
"response_options": { "mode": "single" },
"skip_if": { "when_answer_equals": "no" }
},
{
"task_type": "rating",
"instruction": "Rate how realistic this clip looks.",
"response_options": { "scale": [1, 2, 3, 4, 5] }
}
],
"datapoints": [
{
"media_per_step": {
"0": {
"options": [
{ "id": "yes", "text": "Yes" },
{ "id": "no", "text": "No" }
]
},
"1": {
"subject": [
{ "url": "dp://e7d921ab3c45/sample.mp4", "type": "video" }
]
}
}
}
]
}'Common errors
| Status | Cause |
|---|---|
422 | steps is empty or has more than 5 entries. |
400 | A standalone field (instruction, dimensions, dimension_weights, response_options) is set alongside steps, or task_type is set to anything other than "comparison" (default) or "chain". |
400 | A step uses an unknown task_type, has invalid response_options, or rejects its datapoint media (handler validation; the error names the step index and task type). |
400 | skip_if is set on the last step. |
400 | A skip_if predicate references a submission field the step's task type doesn't declare. |
422 | A datapoint's media_per_step uses non-digit keys (e.g. "a" instead of "0"). |
400 | A datapoint's media_per_step is missing required keys or has extra keys — it must cover every step index exactly. |
Retrying
Chain jobs support POST /jobs/{id}/retry. Retrying a failed step re-queues the entire chain instance — all of its sibling steps — so the walk runs again from the start. The one exception: an instance can't be retried if one of its steps was blocked by content review.
Next
- Survey Planner: describe a chain in plain English and let the planner build it
- Task types: the building blocks each step is built from
- API Reference: Jobs: full request and response shapes