Batch Recovery
When you submit 1,000 records to a batch API, what happens when only 985 come back? Or when 40 of those 985 have outputs that fail validation? Batch recovery handles both problems automatically through a two-phase process.
The Two Phases
Batch recovery runs after the initial batch completes. It addresses two distinct failure modes in sequence:
Phase 1 (Retry) recovers records the provider did not answer — ones it dropped outright (network errors, timeouts, silent failures) and ones it returned with a per-record error and no content. The same request is resubmitted unchanged.
Phase 2 (Repair) fixes records where the LLM responded but produced output that fails the action's expect: suite — schema violations, failed rules. The prompt is modified with the failure feedback before resubmitting.
Phase 1: Retry Missing Records
After retrieving batch results, the system compares the expected record IDs against the ids the provider actually answered. A record returned with an error, or with null content, was not answered. When the action has retry on, any gap triggers the retry loop. Retry is off until a retry: block turns it on (see Configuration); without it, a record the provider did not return is failed at once (see A record nothing sends again).
How It Works
- Collect all
custom_idvalues from the context map (expected) - Collect the
custom_idvalues of results the provider answered successfully. A result returned with an error carries the record's realcustom_idbut no content, so it does not count as answered - Compute the difference — these are the missing records
- For each retry attempt (up to
max_attempts):- Build new records from the context map for missing IDs
- Submit as a new batch
- Poll until complete
- Merge the results back, replacing any earlier answer for the same record
- Update the missing set
- If records remain missing after all attempts, mark them with exhaustion metadata
A retry is prepared again from each record's stored context, with the action's dependencies
and version, so its prompt reads the same upstream answers and its guard decides as it did for
the first request. The run's workflow.* values are not handed to it yet. A record whose
prompt reads them is not retried: nothing is sent for it, and it is failed as below.
A guard that reads them cannot be evaluated on a retry, and the collect run fails.
A record nothing sends again
A record the provider left out of its results that no retry will send again is failed, with
the reason batch_not_returned. That is a record of an action with retry off, or one whose
retry batch did not go out, because its prompt could not be rendered (as for a prompt that
reads workflow.*) or the provider refused it.
The action completes with failures (completed_with_failures), the actions that read it
skip the record, and agac retry sends it again. Once its answer is
collected, the actions that read it answer it too.
Configuration
Retry is configured at the action or defaults level. It is off for an action when neither
the action nor defaults: gives a retry: block:
defaults:
retry:
enabled: true
max_attempts: 3 # Retry up to 3 times
on_exhausted: return_last # or "raise"
Per-Record Tracking
The system tracks failure counts per record, not globally. If record A succeeds on retry attempt 1 but record B needs all 3 attempts, each gets its own count:
{
"_recovery": {
"retry": {
"attempts": 2,
"failures": 1,
"succeeded": true,
"reason": "missing",
"timestamp": "2024-06-15T10:30:45Z"
}
}
}
Exhaustion
When a record exhausts all retry attempts:
on_exhausted: return_last— record is marked with exhaustion metadata, workflow continues without iton_exhausted: raise— the output file is written first, then the run stops. Batch writes once at the end, so halting mid-conversion would discard every record that had already converted cleanly; the halt is carried to the caller and raised after the write
Both policies apply to every record that spends its attempts, but the two exhausted shapes
do not land in the same disposition. The split is whether a result row came back at all. A record the provider never
returned has nothing to report, so it becomes an EXHAUSTED tombstone dispositioned
exhausted_after_N_attempts. A record that came back — with an error, or with null
content and no message at all — is dispositioned FAILED, carrying whatever the
provider said (or a generic failure when it said nothing) plus the same exhausted
retry metadata under _recovery.retry. The provider's
message is the more useful signal of the two, so it is preserved rather than replaced by an
empty tombstone. Counting retry exhaustion means counting both — filter on
_recovery.retry.succeeded == false, not on the disposition alone.
Phase 2: Validate and Repair
After Phase 1 ensures all recoverable records are present, Phase 2 checks whether the outputs are
actually valid. This runs when the action carries an expect:
block with a repair policy.
How It Works
A repair round is a whole provider batch, so it defers rather than blocking:
- Run the action's expectation suite over every returned record, including the structural gate that checks schema conformance
- Records that pass graduate into a pool and are never evaluated again — a later round does not re-spend the judge budget on them
- Records that fail are collected into a repair batch, each carrying its own regeneration prompt
- The batch is submitted and the run defers; the next pass resumes into the same loop
- When
max_iterationsrounds are spent,on_exhausteddecides what ships
A record the provider never returns is reconstructed as a failure rather than left to become a silent passthrough, and a record with no content at all failed at the provider rather than at its expectations, so it keeps its own error.
Recovery Metadata
Every record that goes through recovery gets a _recovery field in its output. This field is automatically excluded from content extraction — downstream actions never see it, but it's available for auditing and debugging.
Structure
{
"_recovery": {
"retry": {
"attempts": 3,
"failures": 2,
"succeeded": true,
"reason": "missing",
"timestamp": "2024-06-15T10:30:45Z"
},
"expectations": {
"attempts": 2,
"failed": []
}
}
}
Fields
Retry metadata (present when transport-layer recovery occurred):
| Field | Type | Description |
|---|---|---|
attempts | integer | Total attempts made (including initial) |
failures | integer | Number of failed attempts before success |
succeeded | boolean | Whether retry ultimately succeeded |
reason | string | Why retry was needed: missing, network_error, rate_limit, timeout |
timestamp | string | ISO 8601 timestamp of the recovery |
Expectations metadata (_recovery.expectations, present when the repair loop exhausted):
| Field | Type | Description |
|---|---|---|
attempts | integer | Iterations spent, counting the first generation |
failed | list | Ids of the expectations still failing when iterations ran out |
Evaluation metadata (_recovery.evaluation, present when a batch record graduated through the loop):
| Field | Type | Description |
|---|---|---|
passed | boolean | Whether the record graduated |
strategy_name | string | Which evaluation strategy judged it |
Serialization
Recovery metadata survives serialization. When batch results are saved to disk and reloaded (e.g., for agac batch retrieve), both retry and expectations metadata are preserved through the round-trip.
Example: Full Recovery Flow
Submit 100 records. 95 come back. 10 of the 95 fail validation.
Submit 100 records
├── Phase 1: Retry
│ ├── 5 missing → resubmit
│ ├── Attempt 1: 4 recovered, 1 still missing
│ ├── Attempt 2: 1 recovered
│ └── Result: 100 records (all recovered)
│
└── Phase 2: Repair
├── Validate 100 results → 10 fail
├── Attempt 1: resubmit 10 with feedback → 8 pass
├── Attempt 2: resubmit 2 with feedback → 1 passes
└── Result: 99 passed, 1 exhausted (on_exhausted: return_last)
Final output:
94 records — clean (no recovery needed)
5 records — _recovery.retry present
1 record — _recovery.retry + _recovery.expectations (both phases)
See Also
- Retry & Error Handling — Transport-layer retry for online mode
- Expectations — Output rules and the repair loop
- Run Modes — Online vs batch execution
- Output Format — Output structure and
_recoveryfield