Skip to main content
Version: 2026.1

Agent Template Generation

This is the write path for Twig templates: how an agent's proposed template is validated, test-rendered, persisted, and wired into Pimcore's rendering stack. The user-facing flow is described in Features → Agent Templates; this page covers the internals.

Private root and Twig namespace

Approved templates are written to a private root - var/agent-templates/ by default - as one flat file per slug:

var/agent-templates/
├── landing.html.twig
└── shared-header.html.twig

The directory is never web-served; generated files stay out of hand-authored project source so ownership is clear and the write path stays reversible.

AgentTemplateTwigLoader sits in Pimcore's Twig loader chain and resolves @PimcoreAgentTemplates/<slug>.html.twig to <private-root>/<slug>.html.twig. Cross-template {% include %} / {% extends %} must use this namespaced form - bare relative paths are rejected, because Twig's namespace loader cannot resolve relative paths from inside a namespaced template.

AgentTemplateProvider contributes approved templates to the Studio document-template dropdown through Pimcore's pimcore.template_provider extension point, filtered by type: page templates for Page documents, snippet for Snippet documents. partial templates are include-only and never appear.

Validation pipeline

propose_agent_template runs a pure, write-free validation before a proposal is stored:

  1. Forbidden-construct rejection. TemplateValidator parses the template and walks the AST with ForbiddenConstructVisitor, rejecting any tag, filter, or function outside a positive allowlist.
  2. CSS scan. <style> blocks and inline style="" attributes are rejected for external @import, external url(), expression(, and -moz-binding.
  3. Path-traversal guards. Slugs are constrained to [a-z0-9_-]; .. segments in template names and include/extends targets are rejected; every write target is verified to resolve under the private root.

Implicit pimcore_* allow-listing

Pimcore registers every document editable through one pimcore_* wildcard function resolved by a prefix loader - there is no enumerable list. Mirroring Pimcore core's own sandbox, the bundle allows any pimcore_* function by prefix (PimcoreFunctionPolicy), so every editable - current or future, core or bundle - works without being listed. The sole denied pimcore_* function is pimcore_inc. The same policy backs both the propose-time validator and the render-time sandbox.

Sandboxed test-render

A valid template is smoke-rendered in a sandboxed Twig environment (SandboxSecurityPolicy, the same allowlist as the validator). The renderer populates context through a configurable controller (TestRenderController by default) and wires the framework's form runtime so {{ form(form) }} renders against a throwaway form view. Output is bounded by a byte cap (checked after render) and a wall-clock timeout (the hard stop). Only a template that validates and renders reaches the reviewer; nothing is written to disk.

Persistence

On approval the resolver writes the file to a temp path and renames it into the private root atomically, then creates or updates the AgentTemplate entity (bundle_agent_templates: slug, label, type, status, owner, include_deps). A failure before the rename leaves no on-disk trace. Update and delete proposals are user-scoped - an agent may only change templates created by the current Studio user's sessions; Pimcore admins bypass the check.

Sample documents and clone-and-reassign

A document counts as a sample for a slug when it lives under the configured sample folder and its template field equals @PimcoreAgentTemplates/<slug>.html.twig - there is no registration step. The approve resolver returns the resolved contextDocumentId, so a just-created sample can be opened and refreshed.

Clone-and-reassign lets the agent refine a document that uses a non-agent (project) template: it proposes a create with reassignDocument=true, and on approve the resolver writes the new template and repoints the document's template field. The reassign flag is recomputed at propose time and dropped to false when the document already uses the slug, and the resolver is idempotent on retry. Reassigning only changes the template field - it has no effect if the document's controller renders a hard-coded template instead of the document's configured one.

Drift-guard lint

A Pimcore upgrade can change the editable registry or the sandbox allowlist after templates were approved. The lint command re-validates every persisted template and cross-checks each entity's include_deps against the actual sources:

bin/console pimcore-agent:agent-templates:lint            # errors fail (exit 1); warnings advisory
bin/console pimcore-agent:agent-templates:lint --strict # warnings also fail - for CI

Further reading