Daemion docs

How do I create a renderer extension?

A renderer extension tells Daemion how to display a specific type of structured content in the chat interface — instead of showing raw text or JSON, the app renders it as a custom component with actions, formatting, and interactivity.


What is a renderer extension?

A renderer is an extension with type: "renderer" that maps a content type identifier to a display component. When an agent produces a turn containing that content type, the app passes the structured data to the renderer instead of the default text display. Common examples: proposal cards with accept/reject buttons, diff viewers with syntax highlighting, or Mermaid diagram renderers.

Renderer definitions use the generic Record<string, unknown> schema — there is no fixed field set. The convention is to include a contentType field (the identifier the agent tags its output with) and any renderer-specific config.


When would I use a renderer extension?

Use a renderer when an agent regularly produces a specific structured output that benefits from a custom display. Scenarios:

  • Your daily-digest job produces a structured summary object — render it as a formatted card instead of a JSON block
  • An agent proposes code changes — render a diff viewer with line-by-line annotations
  • A job outputs Mermaid diagram markup — render inline SVG diagrams directly in the thread

If the output is one-off or purely text, a renderer adds unnecessary complexity. Renderers are for recurring, structured content types you want to display consistently.


How do I create a renderer through chat?

Open the Daemion app, start a new thread, and describe the content type and how it should look:

text

Create a renderer for proposal cards that shows accept and reject buttons.

Daemion will register the renderer and explain how to use it:

text

User: Create a renderer for proposal cards that shows accept and reject buttons.

Daemion: I’ll create a proposal-card renderer for you.

[Creating extension…]

Done. The “proposal-card” renderer is registered and enabled. When an agent turn contains content tagged as “proposal-card”, the app will display it with Accept and Reject action buttons instead of raw text. You can describe changes to the layout or add more actions at any time.

The resulting extension record:

json

{ “id”: “ext_07mno678”, “type”: “renderer”, “name”: “proposal-card”, “description”: “Renders proposal objects with Accept and Reject action buttons.”, “definition”: { “contentType”: “proposal-card”, “actions”: [“accept”, “reject”], “showMetadata”: true, “expandable”: false }, “source”: “agent”, “enabled”: true, “created_at”: “2026-03-31T09:00:00Z”, “updated_at”: “2026-03-31T09:00:00Z” }


How do I create a renderer via the API?

bash
Verified

curl -X POST http://localhost:3001/extensions
-H “Authorization: Bearer $DAEMION_TOKEN”
-H “Content-Type: application/json”
-d ’{ “type”: “renderer”, “name”: “proposal-card”, “description”: “Renders proposal objects with Accept and Reject action buttons.”, “definition”: { “contentType”: “proposal-card”, “actions”: [“accept”, “reject”], “showMetadata”: true, “expandable”: false }, “source”: “user”, “enabled”: true }’

Because renderers use the generic definition schema, any valid JSON object is accepted in definition. Structure it to match what your rendering component expects to receive.


What does the schema look like?

Renderers use GenericDefinitionSchema — defined in src/schema/extension.ts:71:

typescript

// Renderer definition — no fixed shape. // Convention: include a contentType field so the app can route turns to this renderer. type RendererDefinition = Record<string, unknown>;

// Example shape (not enforced by schema): interface RendererDefinitionExample { contentType: string; // matches the tag an agent puts on its structured output actions?: string[]; // action identifiers the component exposes (e.g. “accept”, “reject”) showMetadata?: boolean; // whether to show turn metadata (timestamp, model, etc.) expandable?: boolean; // whether the card can be collapsed/expanded [key: string]: unknown; // any additional config your renderer component reads }

The gateway accepts any JSON object in definition for renderer extensions. Structure and enforcement happen in the rendering component itself, not at the schema level.


Frequently asked questions

Q How does the app know which renderer to use for a given turn?
When an agent produces structured output, it tags the turn with a content type identifier (e.g. proposal-card). The app looks up an enabled renderer whose definition.contentType matches that tag and passes the turn data to it. If no renderer matches, the turn falls back to the default text display.
Q Can I have multiple renderers for the same content type?
Only one enabled renderer per content type is active at a time. If two enabled renderers declare the same contentType, the app uses the most recently updated one. Disable the old renderer before enabling an alternative.
Q Do I need to write any frontend code to create a renderer?
Through chat, no — Daemion scaffolds the renderer extension record and describes the expected data contract to the agent. For custom visual components beyond what the built-in renderer templates support, you can ask Daemion to edit the rendering component files directly.
Q Can a renderer expose interactive actions (buttons, inputs)?
Yes. Declare the action identifiers in your definition.actions array. The rendering component maps those identifiers to UI controls. When a user clicks Accept or Reject, the action is dispatched back to the agent as a new turn in the thread.
Q Can I test a renderer without an agent producing the content?
Yes — POST a turn directly to a thread with the matching content type tag and structured payload via POST /threads/:id/turns. The app will route it through the renderer so you can verify the display without running a full agent conversation.

What can go wrong

Renderer registration and routing errors

400 &#123;"error": "validation failed", "details": [...]&#125; — The top-level extension fields failed validation (e.g. name is empty or too long, type is misspelled). The definition itself is not validated beyond being a JSON object — any valid JSON is accepted.

403 &#123;"error": "cannot disable essential extensions"&#125; — Essential built-in renderers cannot be disabled.

Renderer registered but turns still show as raw text — Check that the agent is tagging its output with the exact contentType string declared in the renderer’s definition. Content type matching is case-sensitive. Also verify the renderer is enabled (GET /extensions?type=renderer&enabled=true).

Multiple renderers conflict — If two enabled renderers declare the same contentType, behavior is undefined. Disable all but one. Use GET /extensions?type=renderer to list all registered renderers and check for duplicates.


What’s next?