Lifecycle
Overview¶
As Instructor processes your request, it moves through a well-defined series of stages. Understanding this lifecycle helps when debugging unexpected output or building custom extensions.
Request Lifecycle Steps¶
1. Build the Request¶
The StructuredOutput facade collects your messages, response model, system prompt,
examples, model overrides, and options into an immutable StructuredOutputRequest.
2. Analyze the Response Model¶
The ResponseModelFactory inspects the responseModel parameter and determines how
to build a schema. Depending on the type of input, it follows one of several
paths (class string, object instance, raw JSON Schema array, schema provider, etc.).
The result is a ResponseModel containing the target class, a Schema, and the
rendered JSON Schema for the provider.
3. Translate to Provider Schema¶
The schema is rendered into the format required by the selected output mode:
| Output Mode | Schema Delivery |
|---|---|
Tools |
Wrapped in a tool-call function definition |
Json |
Included in the system/user prompt as text |
JsonSchema |
Sent via the provider's response_format parameter |
MdJson |
Included in the prompt, response expected in a json codeblock |
Text |
Schema included in the prompt; JSON extracted from unstructured text |
Unrestricted |
No output constraints; extraction is best-effort |
4. Execute the Inference Request¶
The PendingStructuredOutput delegates to the configured CanCreateInference
implementation (via the Polyglot inference layer). For streaming requests, the
response arrives as a series of chunks.
5. Extract Structured Data¶
The CanExtractResponse extractor pulls JSON data from the raw inference response.
The extraction strategy depends on the output mode -- for Tools mode it reads
tool-call arguments; for Json/JsonSchema it parses the content directly; for
MdJson it extracts from a fenced code block.
6. Deserialize into the Target Shape¶
The extracted array data is deserialized into the target PHP class (or returned as
an array if intoArray() was specified). Classes implementing CanDeserializeSelf
can override this step entirely.
7. Validate the Result¶
The deserialized object is validated. Built-in validation uses Symfony Validator
constraints declared on the response model class. Classes implementing
CanValidateSelf can provide their own validation logic.
8. Transform (Optional)¶
If the response model implements CanTransformSelf, the validated object is
transformed into a different value before being returned to the caller. This is
how helpers like Scalar unwrap a wrapper class into a plain PHP scalar.
9. Return the Result¶
The final value is wrapped in a StructuredOutputResponse (or yielded as a stream
of partial responses) and returned to the caller.
Retry Loop¶
Steps 4 through 8 run inside a retry loop. When validation or deserialization fails:
- The error message is formatted as feedback for the LLM.
- The feedback is appended to the conversation as a retry message.
- The LLM is called again with the updated context.
- The cycle repeats until the response passes or
maxRetriesis exhausted.
The retry budget is configured via StructuredOutputConfig::maxRetries. A value of
0 (the default) means a single attempt with no retries. The total number of
attempts is always maxRetries + 1.
When the retry limit is reached without a valid response, a
StructuredOutputRecoveryException is thrown.
Streaming Lifecycle¶
When streaming is enabled, the lifecycle diverges after step 4:
- Chunks arrive incrementally from the provider.
- Each chunk is accumulated into a partial JSON string.
- The partial JSON is deserialized into a partial object (best-effort).
- A
StructuredOutputResponsesnapshot (marked as partial) is emitted. - When the stream completes, the final response goes through the full validation and transformation pipeline (steps 7-8).
- The finalized response is emitted as a non-partial
StructuredOutputResponse.
Partial updates are available through StructuredOutputStream::partials(),
sequence(), or responses().