POST /api/v1/forward/batches
Forward reads the input file, parses the JSONL structure, and persists the initial task counts before returning validating. External dependencies such as file resources are pre-validated in the background; only tasks that pass pre-validation can wait for scheduling. By default, a Batch is eligible for scheduling only inside the idle window. Set ignore_idle_window=true to remove that time-window restriction. When processing finishes, Forward produces output.jsonl and, when failed lines exist, error.jsonl.
| Header | Required | Description |
|---|
Authorization | Yes | Bearer <PAT or SAT> |
Content-Type | Yes | application/json |
Idempotency-Key | No | Optional idempotency key for unsafe requests. |
Creating a batch requires a JSONL input file uploaded through the CAS Files API to obtain a file_id.
JSONL format — one JSON object per line, one line per independent task:
{"custom_id":"task-001","template_id":"tmpl_example001","identity_id":"idn_example001","body":{"input":"analyze the report","resources":[{"type":"file","file_id":"opaque-report-id","mount_path":"/data/input/report.pdf"}]}}
{"custom_id":"task-002","template_id":"tmpl_example001","identity_id":"idn_example001","body":{"input":"summarize the meeting notes","resources":[{"type":"file","file_id":"opaque-notes-id"}]}}
| Field | Type | Required | Description |
|---|
custom_id | string | Yes | Caller-defined identifier. Must be unique within a single batch and is used to map result lines. |
template_id | string | Yes | Forward Template ID that drives execution. |
identity_id | string | Yes | Forward Identity ID that provides the execution identity. |
body | object | Yes | Request body passed to Session. It currently contains required input and optional resources. |
body.input | string | Yes | Input sent to the Agent for this task. |
body.resources | array | No | File resources added to this task. Omit it or pass an empty array to add no line-level files. |
body.resources[].type | string | Yes | Resource type. Currently only file is supported. A missing or different value makes the line an invalid_line. |
body.resources[].file_id | string | Yes | File ID returned by the Files API. Treat it as an opaque string; after trimming, it must not be empty. |
body.resources[].mount_path | string | No | Mount path in the Agent container. It must be a canonical absolute path. When omitted, /data/workspace/<filename> is generated from the filename. |
Each resources item must be a JSON object containing only type, file_id, and mount_path. A wrong field type, unknown field, relative path, non-canonical path containing .., or control character makes the entire line an invalid_line; the resource is not silently ignored.
Line-level files are appended to the effective Template and Identity resources; they do not replace existing resources. A duplicate file_id, duplicate mount path, or path overlap with a Template, Identity, or repository resource causes the task to fail with config_error.
The file must be accessible to the identity that creates the Batch and must be in a mountable ready state. A Batch does not copy or lock files or extend their lifetime. If a file is deleted, expires, or changes after pre-validation, Session creation validates it again and the task may still fail.
Upload the file:
curl -X POST 'https://api.qoder.com/api/v1/cloud/files' \
-H "Authorization: Bearer $QODER_ACCESS_TOKEN" \
-F "file=@batch_input.jsonl" \
-F "purpose=session_resource"
purpose=session_resource is required for Batch input files. If this field is omitted, the Files API stores the file with user_upload; CAS does not allow server-side downloads of such files, and Create Batch returns 400 invalid_input_file.
The returned id is the input_file_id for Create Batch.
Body parameters
| Parameter | Type | Required | Description |
|---|
input_file_id | string | Yes | JSONL file ID uploaded via the Files API. |
completion_window | string | Yes | Completion window: 24h, 48h, or 72h. The batch moves to expired when the window elapses. |
metadata | object | No | Caller-supplied metadata. Up to 16 keys, any JSON value; the serialized object must be ≤ 2KB, each key ≤ 64 characters, and no NUL (U+0000) is allowed. |
ignore_idle_window | boolean | No | Whether to ignore the idle window. Defaults to false when omitted. Only JSON booleans true and false are accepted; strings and numbers are not coerced. |
ignore_idle_window is a strict boolean field. null, "true", 1, [], and {} return HTTP 400 invalid_request_error / invalid_request with the message ignore_idle_window must be a boolean.
Example requests
Regular Batch (uses the idle window by default)
Omitting ignore_idle_window is equivalent to explicitly passing false:
curl -s -X POST 'https://api.qoder.com/api/v1/forward/batches' \
-H "Authorization: Bearer $QODER_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"input_file_id": "file_input001",
"completion_window": "24h"
}'
Batch that ignores the idle window
curl -s -X POST 'https://api.qoder.com/api/v1/forward/batches' \
-H "Authorization: Bearer $QODER_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: idem_batch_001" \
-d '{
"input_file_id": "file_input001",
"completion_window": "24h",
"metadata": {
"source": "data_pipeline",
"job_id": "12345"
},
"ignore_idle_window": true
}'
Example response
HTTP 200 OK
{
"id": "batch_example001",
"object": "batch",
"status": "validating",
"input_file_id": "file_input001",
"completion_window": "24h",
"ignore_idle_window": true,
"created_at": "2026-07-07T07:25:01Z",
"expires_at": "2026-07-08T07:25:01Z",
"request_counts": {
"total": 2,
"pending": 2,
"running": 0,
"completed": 0,
"failed": 0,
"cancelled": 0,
"expired": 0
},
"usage": null
}
Create Batch returns a validating snapshot after synchronously parsing the JSONL structure, checking existing unattended Template/Identity policies, and persisting tasks. Therefore, request_counts contains the real initial counts rather than all-zero placeholders. Individual file lookups and merged-resource conflict checks run in the background; only after they pass can the Batch move to queued. output_file_id and error_file_id appear only after the Batch reaches a terminal state and are omitted from the create response.
If 3 of 100 lines fail synchronous structural parsing, the create response has total=100, pending=97, and failed=3. Background resource pre-validation may subsequently move some tasks from pending to failed, but the counter invariant always holds.
When global capacity is full
When the global number of processing Batches has reached capacity, Create Batch still returns HTTP 200 OK with validating. A dynamic snapshot reports the current waiting reason:
{
"id": "batch_capacity_waiting001",
"object": "batch",
"status": "validating",
"input_file_id": "file_input001",
"completion_window": "24h",
"ignore_idle_window": true,
"queue_reason": "global_capacity",
"created_at": "2026-07-07T07:25:01Z",
"expires_at": "2026-07-08T07:25:01Z",
"request_counts": {
"total": 2,
"pending": 2,
"running": 0,
"completed": 0,
"failed": 0,
"cancelled": 0,
"expired": 0
},
"usage": null
}
Response fields
| Field | Type | Description |
|---|
id | string | Batch ID with prefix batch_. |
object | string | Always batch. |
status | string | Batch status. See the status table below. |
input_file_id | string | Input JSONL file ID. |
completion_window | string | Completion window: 24h, 48h, or 72h. |
ignore_idle_window | boolean | Always returned. Whether the Batch ignores the idle window. Historical Batches and Batches created without the field use false. |
queue_reason | string | Optional dynamic snapshot of the current queue reason. May be returned only in validating or queued. |
created_at | string | Creation time, RFC 3339. |
expires_at | string | Expiration time, created_at + completion_window. |
request_counts | object | Aggregate task counters. |
usage | object/null | null in the create response. In subsequent batch detail, list, and cancellation responses, returns aggregate Credit usage after at least one subtask has valid CAS Session usage. |
usage.total_credits | number | Sum of persisted subtask total_credits. The unit is CAS Credit, not tokens or currency. Explicit zero values are preserved. |
metadata | object | Caller-supplied metadata. |
Batch status
| Status | Description | Type |
|---|
validating | JSONL-line and synchronous configuration results have been persisted; effective configuration and file resources are being pre-validated in the background. Tasks have not entered the execution queue. | Non-terminal |
queued | Resource pre-validation has completed and at least one executable task exists. Waiting for the Scheduler to activate it according to the idle window, capacity, and owner mutual-exclusion rules. | Non-terminal |
processing | Executing tasks. | Non-terminal |
cancelling | Cancellation in progress. Waiting for running tasks to finish. | Non-terminal |
expiring | Expiration in progress. Waiting for running tasks to finish. | Non-terminal |
finalizing | Generating output files. | Non-terminal |
completed | All tasks executed. Output files generated. | Terminal |
failed | Batch-level failure, such as an unreadable input file, persistence failure, or unrecoverable resource-validation process. | Terminal |
cancelled | Cancelled by the user. | Terminal |
expired | The completion_window elapsed. | Terminal |
Scheduling and queue reasons
owner is the business ownership scope associated with the credential: a PAT is scoped to the current user, while an administrator SAT is scoped to its organization and workspace.
ignore_idle_window=true changes only the Batch's time-window eligibility. It does not mean immediate execution or higher priority. The following rules still apply:
- At most one
processing Batch is allowed for the same owner.
- Global Batch capacity and global Task capacity are unchanged.
- Only Batches that finish resource validation and move from
validating to queued can be activated.
- Inside the idle window, regular Batches and Batches that ignore the window share FIFO order by
created_at and ID.
completion_window still starts at created_at and includes validation, queueing, and execution time; ignoring the idle window does not extend or reset it.
queue_reason is a dynamic snapshot calculated at read time. It may change immediately after a response and is omitted if the snapshot cannot be loaded, without affecting the main API response. The first matching value in the following priority order is returned:
| Priority | Value | Meaning |
|---|
| 1 | idle_window | The current time is outside the idle window and this Batch does not ignore it. |
| 2 | owner_processing | Another Batch for the same owner is already processing. |
| 3 | global_capacity | The global number of processing Batches has reached capacity. |
| 4 | scheduler_pending | The Batch is queued, none of the first three conditions blocks it, and it is waiting for Scheduler activation. |
queue_reason applies only to validating and queued. In validating, only the first three confirmed external blockers can be returned. When none applies, the field is omitted; scheduler_pending is not returned. It is omitted for processing, finalizing, cancelling, expiring, and all terminal states.
When global Batch capacity is full, Create Batch can still successfully return validating. After validation, the Batch remains queued and reports global_capacity as the current waiting reason.
Request counts
| Field | Type | Description |
|---|
total | integer | Total lines, including lines that failed validation. |
pending | integer | Lines that have not started. During validating, these are accepted tasks still awaiting resource pre-validation and have not entered the execution queue. |
running | integer | Lines currently executing. |
completed | integer | Lines that finished successfully. |
failed | integer | Lines that failed permanently, including validation failures. |
cancelled | integer | Lines terminated by cancellation. |
expired | integer | Lines terminated by expiration. |
The invariant total = pending + running + completed + failed + cancelled + expired always holds.
Validation and enqueue gate
Create Batch synchronously performs the following work inside the request:
- Read the input file and parse each JSON line.
- Validate required fields,
custom_id uniqueness, and the body.resources structure.
- Resolve the Template and Identity, and apply the existing unattended tool-permission policy checks.
- Persist lines that pass synchronous validation as
pending, and persist failed lines as failed with invalid_line, config_error, or permission_denied.
- Persist the real initial
total, pending, and failed counts.
After the response, Forward uses the Batch creator's identity to resolve effective Template/Identity resources in the background, then performs file lookups, default-path generation, and merged-resource conflict checks. During pre-validation the Batch remains validating; it cannot enter the execution queue or create a Session. When pre-validation finishes:
- At least one valid task remains:
validating → queued → processing.
- Every task failed:
validating → finalizing → completed, and result files are generated.
- Cancellation or expiration happened first: resource validation does not reactivate or enqueue the Batch.
A failed line does not block other lines. It remains in output.jsonl and is also written to error.jsonl. If no Session was created, session_id is omitted and body.resources preserves the original request content.
Task error classifications
| Code | Meaning | Resource lookup retries |
|---|
invalid_line | Invalid JSONL structure, resource fields, type, file_id, or explicit mount path. | No |
config_error | The file returns 404/410, is not ready, cannot be mounted, or conflicts with another merged resource. | No |
permission_denied | The upstream service explicitly returns 401/403. | No |
transient_error | A file lookup times out or returns 429/5xx after up to three backoff retries. | Up to 3 |
To prevent enumeration, the upstream service may use 404 for both “file does not exist” and “file is not visible to this caller.” Forward does not infer file ownership or existence from a 404. It returns a safe config_error that does not disclose resource metadata; only an explicit 401/403 is classified as permission_denied.
Error codes
| HTTP | Type | Code | Trigger |
|---|
| 400 | invalid_request_error | invalid_request | input_file_id missing, invalid completion_window, metadata too large, or ignore_idle_window is not a JSON boolean. |
| 400 | invalid_request_error | invalid_input_file | The input file cannot be downloaded by Batch. Upload it with purpose=session_resource. |
| 409 | conflict_error | batch_already_processing | ignore_idle_window=true and another Batch for the same owner has the exact status processing. |
| 429 | rate_limit_error | rate_limit_exceeded | The user has reached the maximum number of unfinished batches. |
| 401 | authentication_error | authentication_required | PAT or SAT is invalid or expired. |
Same-owner processing conflict
Create Batch returns HTTP 409 only when the request has ignore_idle_window=true and another Batch for the same owner has the exact status processing:
{
"type": "error",
"request_id": "req_example001",
"error": {
"type": "conflict_error",
"code": "batch_already_processing",
"message": "Another Batch is already processing for this owner.",
"batch_id": "batch_existing001"
}
}
error.batch_id is the ID of the currently processing Batch for the same owner. This conflict check runs before the unfinished-Batch quota check and before all file and persistence side effects. A rejected request does not create a Batch or Task, does not read, parse, or validate the input file, and does not send an expiration message. validating, queued, finalizing, cancelling, expiring, and terminal states do not trigger this 409.
Notes
- A single batch supports up to 10,000 JSONL lines.
- Global concurrency is capped at 50 tasks.
- By default, scheduling runs inside the idle window (default 22:00–08:00, configurable server-side).
ignore_idle_window=true removes only this time-window restriction.
- After creating a batch, the client should poll
GET /api/v1/forward/batches/{batch_id} until a terminal state is reached.