LLM Structured Output (Workflow LLM Node)#
Overview#
The workflow LLM node supports JSON schema-based structured output: users define a schema, Dify enforces it at inference time (either natively or via prompt injection), then parses and repairs the model's response. The feature is toggled by structured_output_enabled on the node data and the schema is stored in structured_output.schema .
The core implementation lives in api/core/llm_generator/output_parser/structured_output.py. The single public entry point is invoke_llm_with_structured_output(), which accepts a json_schema mapping and returns either an LLMResultWithStructuredOutput or a generator of LLMResultChunkWithStructuredOutput objects.
Schema Definition (Frontend)#
Users define the output schema in the LLM node panel via a modal with two editing modes: Visual Editor and JSON Schema (raw text) . The modal validates schemas client-side (depth limit via JSON_SCHEMA_MAX_DEPTH, Draft-7 compliance, required additionalProperties: false) before saving .
The TypeScript type for a stored schema is SchemaRoot: a top-level object with properties, optional required, and additionalProperties: false. Individual fields (Field) support types including string, number, boolean, object, array, and enum .
An AI-assisted schema generator (backed by SYSTEM_STRUCTURED_OUTPUT_GENERATE) lets users describe fields in natural language and receive a valid JSON schema .
Native Support vs. Prompt-Based Fallback#
The decision point is model_schema.support_structure_output on the AIModelEntity:
Native path (support_structure_output = True β e.g., GPT-4o, Gemini):
_handle_native_json_schema()is called.- The schema is serialized and set as the
json_schemamodel parameter. - If the model's parameter rules include
response_formatwith ajson_schemaoption, it is also set .
Prompt-based fallback (all other models):
_set_response_format()selectsJSONorjson_objectmode based on model rules._handle_prompt_based_schema()prepends a system message usingSTRUCTURED_OUTPUT_PROMPT(a template with a{{schema}}placeholder). The serialized schema is injected and the original system prompt (if any) is appended afterward.
Model-Specific Schema Transformations#
Before passing a schema to a native-mode model, _prepare_schema_for_model() applies transformations:
| Model | Transformation |
|---|---|
| All | convert_boolean_to_string() β replaces "type": "boolean" with "type": "string" recursively |
| Gemini | remove_additional_properties() β strips all additionalProperties fields recursively |
| Ollama | Schema passed as-is (no wrapping) |
| Default (OpenAI-style) | Wrapped as {"schema": <schema>, "name": "llm_response"} |
Response Parsing and JSON Repair#
_parse_structured_output() handles all response parsing:
- Tries
TypeAdapter(Mapping).validate_json(result_text)for strict parse. - On
ValidationError, falls back tojson_repair.loads(result_text). - If
json_repairreturns alist(common with reasoning models like DeepSeek-R1 that emit<think>\n\n</think>\nprefixes), the firstdictin the list is used . - Any other result raises
OutputParserError.
Streaming vs. Non-Streaming#
Non-streaming: The full response text is parsed once and returned as LLMResultWithStructuredOutput.structured_output .
Streaming: Text chunks are accumulated in a generator. Each chunk yields LLMResultChunkWithStructuredOutput without structured output. The final sentinel yield includes the parsed structured_output from the full accumulated text .
Key Source Files#
| File | Purpose |
|---|---|
api/core/llm_generator/output_parser/structured_output.py | All parsing, schema prep, fallback, and repair logic |
api/core/llm_generator/prompts.py | STRUCTURED_OUTPUT_PROMPT (fallback system prompt template) and SYSTEM_STRUCTURED_OUTPUT_GENERATE (AI schema generator prompt) |
web/app/components/workflow/nodes/llm/types.ts | LLMNodeType, StructuredOutput, SchemaRoot, Field TypeScript types |
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx | JSON schema editor modal (visual + raw JSON views, client-side validation) |