Skip to main content
Version: 2026.1

Document Schema

Documents and areabricks don't have a class definition - their editable surface is whatever the rendering Twig template happens to call pimcore_input(…), pimcore_image(…), pimcore_areablock(…), etc. on. Two consequences for an LLM:

  1. To write to a document, it has to know what the template offers - names, types, addressing - without rendering it.
  2. Some Twig patterns (pimcore_iterate_block, conditionals, computed names) cannot be fully recovered from static analysis, so worked examples a developer authored once compensate.

The bundle gives LLMs that information by combining three signals: AST analysis of Twig templates, the Pimcore editable registry (types + value shapes), and co-located JSON example fixtures. The same machinery powers get_document_schema(documentId) and get_area_brick(brickId) in the pimcore-documents-read MCP group.

What the LLM receives

When the LLM calls get_area_brick("standard-teaser"), the response combines four sources:

SourceCarries
AST of the brick's view templateFlat list of editables ({name, type} per call site) + blocks[] + areablocks[] containers
Pimcore EditableLoaderInterface::supports()Classifies each pimcore_<type> call as a real editable (vs. helpers like pimcore_iterate_block); third-party editables register without bundle changes
EditableHintRegistry (provider-based)writeFormat + a populated example per editable type - same mechanism as data objects (Field Hint System)
Co-located fixtures (<template>.examples/)Optional _meta.json description + per-variant JSON files showing realistic editable combinations - the primary signal for small LLMs

The LLM applies one substitution rule when consuming the response: replace each <index> placeholder in an editable name with a concrete integer. That covers loop iteration (image_<index>image_0) AND Pimcore's iterate-block prefix rule (images:<index>.imageimages:0.image) - so the agent doesn't need a separate concept of "block-contained editable."

The MCP tool surface

Nine document tools across /pimcore-mcp/agent/pimcore-documents-{read,write,direct-write}:

GroupToolUse
readsearch_documentsFind a document by path / key / fulltext
readget_documentCurrent state - opt in to includeContent / includeSettings / includeProperties (and optionally targetGroupId to read a personalization variant)
readget_document_schemaWhat's POSSIBLE - editables[] (every editable plus areablock + block containers with their availableBricks palette) + settings[] + optional examples[] / templateDescription
readget_area_brickOne or more bricks' schema in a single call - comma-separated ids → {results, errors}, each result with editables[] + examples[]
readlist_target_groupsList active personalization target groups (see Target-group variants)
writecreate_documentNew page / snippet / email / link / hardlink / folder. Pair with list_document_types
writelist_document_typesList configured Document Type presets
writepropose_document_updateHITL propose-and-review write - see HITL Proposals
direct-writeupdate_documentDirect editable update - validates pre-write; returns renderErrors[] from a post-save render pass

get_document returns CURRENT state; get_document_schema returns POSSIBLE state. Brick instances under <areablock>:<index>.<editable> are NOT expanded into the document schema - the agent fetches each unique brick type's schema once via get_area_brick (batch every brick id you need into one call). The brick palette previously emitted by the removed list_area_bricks tool now ships inline on each areablock entry (editables[].availableBricks).

Read vs. write split. Read + propose tools live in pimcore-documents-read / …-write. The single bypass-approval tool, update_document, is isolated in pimcore-documents-direct-write so an agent's YAML can grant the propose flow without granting unattended writes - see HITL Proposals → Two write modes.

The AST analyzer

TemplateAnalyzer (in src/Twig/Analyzer/) walks the Twig AST and emits a TemplateSchema:

CapturedWhat it represents
editables[]Every editable call site, as CallSiteEditable {name, type, …}
areablocks[] + areablockAllowed[]pimcore_areablock('content', { allowed: [...] }) + the allowed-brick list
blocks[]pimcore_block(...) / pimcore_scheduledblock(...) call sites
includes[]Static include paths (resolver walks them transitively)

Editable type classification defers to Pimcore's EditableLoaderInterface::supports($type) - the analyzer extracts the suffix from pimcore_<type> calls and asks Pimcore's own registry whether that's a real editable type. So:

  • Standard editable types are recognised automatically
  • Third-party bundles that register custom editables (pimcore_myCustomThing) work without modifying this bundle
  • Non-editable Twig helpers (pimcore_iterate_block, pimcore_url, pimcore_glossary) are skipped automatically

Patterns the analyzer handles

PatternCaptured name
pimcore_input('foo')foo
pimcore_input('foo_' ~ i) inside for i in 0..2foo_<index>
pimcore_iterate_block(pimcore_block('items')) with editables insideBlock items recorded; inner editables get the prefix: items:<index>.<leaf>
{% set blockVar = pimcore_block('X') %} + pimcore_iterate_block(blockVar)Same as above - one hop of {% set %} indirection is followed
{% include 'snippets/foo.html.twig' %}Included template's editables are merged into the parent's schema

Patterns the analyzer cannot fully express

PatternWhat it doesWhat the schema shows
Dynamic block names: pimcore_iterate_block(pimcore_block('contents_' ~ i))Two distinct iteration variables (outer block-name index + inner per-iteration index)Block surfaces as contents_<index>, but inner editables surface as bare names - folding two indices into one <index> placeholder would be ambiguous
pimcore_input(myObject.field)Editable name computed at runtimeNot captured - the AST can't resolve runtime expressions
Nested blocks (block A containing block B)Two real iteration indicesEach block surfaces independently; the inner block's prefix has to be authored in the fixture
Macros passing editables as parametersIndirection through macro argsMacro bodies are walked, but parameter-flow is not traced

When a pattern can't be expressed, the example fixture is the workaround - the runtime LLM reads the worked example and pattern-matches the address shape. See Extending → Template Examples for how authors encode these patterns.

Co-located example fixtures

A Twig template at <dir>/<stem>.html.twig has fixtures in the sibling folder <dir>/<stem>.examples/:

templates/areas/standard-teaser/
├── view.html.twig
└── view.examples/
├── _meta.json ← optional LLM-targeted description
├── all-direct.json ← variant 1
└── mixed-types.json ← variant 2

Same convention for document templates: templates/content/portal.html.twigtemplates/content/portal.examples/.

ExampleLoader resolves the sibling folder per template path on each MCP call (no separate cache - OS-level page cache is sufficient) and folds the result into BrickSchema / DocumentSchema as description (from _meta.json), examples[] (variants), and an optional usageHint.

For everything authoring-related - file format, address rules, value shapes, the dev workflow, the CLI commands, gotchas - see Extending → Template Examples.

Validation

ExampleLinter cross-checks every key in every fixture against the analyzer's editable-name set:

IssueSeverityCause
Fixture key not in analyzer's name seterrorTypo, or stale fixture after a template change
Editable in name set with no example coveragewarningOptional gap - author another variant if useful
Brick has no example folderwarningAdvisory; some bricks (single-editable) may not need one

Run via bin/console pimcore-agent:document-schema:lint. The linter discovers *.examples/ folders by scanning roots configured at pimcore_agent.examples.template_roots (defaults: %kernel.project_dir%/templates and %kernel.project_dir%/bundles). For the dev workflow, see Extending → Template Examples.

Target-group variants (personalization)

When the optional pimcore/personalization-bundle is installed, one document can carry per-target-group variants of its editables - the base content stays canonical, and a variant overrides individual editables when a visitor matches the group. The bundle exposes target groups through list_target_groups and threads a targetGroupId parameter through get_document, propose_document_update, and update_document.

StepTool call
Discover groupslist_target_groups{available: bool, targetGroups: [{id, name}]}. When the personalization bundle is missing, available is false and the other document tools refuse targetGroupId.
Read variant contentget_document(id, targetGroupId: <id>) - every editable carries inherited: true if it falls back to base content, inherited: false if a variant override exists
Author variant contentpropose_document_update(..., targetGroupId: <id>) or update_document(id, targetGroupId: <id>, …) with unprefixed editable names. The backend transparently writes them as persona_-<id>-_<name> rows

Integration touchpoints:

  • src/Personalization/TargetGroupProviderInterface.php - abstraction over Pimcore's TargetGroup\Listing. Implemented by PersonalizationTargetGroupProvider when personalization is installed; falls back to NullTargetGroupProvider (returns available: false) when it is not.
  • src/Mcp/Document/State/DocumentDataInspector.php - strips the persona_-N-_ prefix on read; surfaces inherited per editable so the agent always sees canonical names.
  • src/Mcp/Document/Update/DocumentOverrideApplier.php - re-applies the prefix on write, forces appendEditables=true, rejects payloads that combine targetGroupId with settings or properties. Same applier feeds the proposal preview, so the modal renders the personalized output for the chosen group.

The user-facing workflow is documented in the bundled document-editing SKILL - every agent that opts into the documents groups loads it.

Extension surface

Three places third-party bundles plug in:

AddHow
New editable typeRegister with Pimcore's EditableLoaderInterface - the analyzer picks it up via supports()
Format hint for a custom editable typeImplement EditableHintProviderInterface + tag with pimcore_agent.editable_hint_provider (see Field Hint System)
Example fixtures for project bricks / templatesAuthor JSON files in the sibling <template>.examples/ folder - see Extending → Template Examples

Bundle-shipped areabricks need fixtures alongside the vendor's templates, which means the bundle author owns them. Project-local overlay support for vendor templates is a known gap.