Skip to main content
Tags are JSX elements backed by a plugin. TemplateDX ships five built-in tags (<If>, <ElseIf>, <Else>, <ForEach>, <Raw>) and exposes two ways to register your own: a global static API and a per-instance API on TemplateDX.

Creating custom tags (TypeScript)

Extend TagPlugin

Create a class that extends TagPlugin and implements transform:
import { Node } from 'mdast';
import { TagPlugin, PluginContext } from '@agentmark-ai/templatedx';

interface MyTagProps {
  prefix?: string;
}

class MyTagPlugin extends TagPlugin<MyTagProps> {
  async transform(
    props: MyTagProps,
    children: Node[],
    context: PluginContext
  ): Promise<Node[] | Node> {
    const { nodeHelpers } = context;

    const content = nodeHelpers.toMarkdown({
      type: 'root',
      children: children,
    });

    const prefix = props.prefix ?? '> ';
    const prefixedContent = content
      .split('\n')
      .map(line => prefix + line)
      .join('\n');

    return [{
      type: 'text',
      value: prefixedContent,
    } as Node];
  }
}

The transform method

transform(props, children, context) must return a Promise<Node | Node[]>. context: PluginContext exposes:
FieldTypeDescription
nodeHelpersNodeHelpersAST utilities (see below)
scopeScopeCurrent variable scope; read via scope.get(key), create a child with scope.createChild({ key: value })
createNodeTransformer(scope: Scope) => anyFactory for recursively transforming child nodes under a new scope
tagNamestringName of the tag being processed — set by the transformer from node.name
tagName is typed as string, but the conditional plugin (tag-plugins/conditional.ts) defensively guards if (!tagName) throw .... Built-in tags can rely on tagName being set; advanced consumers that invoke a plugin’s transform directly (outside the transformer) should pass a valid name.

nodeHelpers surface

MemberPurpose
isMdxJsxElement(node)true for flow or text JSX elements
isMdxJsxFlowElement(node)true for block-level JSX
isMdxJsxTextElement(node)true for inline JSX
isParentNode(node)true if the node has a children array
toMarkdown(node)Serialize an AST node back to Markdown/text
hasFunctionBody(node)true for expression nodes containing an arrow function (e.g. <ForEach> callback)
getFunctionBody(node)Returns { body, argumentNames } for arrow-function expression nodes
NODE_TYPESConstants for AST node-type strings (TEXT, PARAGRAPH, MDX_JSX_FLOW_ELEMENT, …)

Register the plugin (static or instance API)

The registry exposes both a static API (process-wide) and an instance API (scoped to a TemplateDX engine). Static (global) registration — the simplest path; everything using the default transform/stringify exports will see it:
import { TagPluginRegistry } from '@agentmark-ai/templatedx';

TagPluginRegistry.register(new MyTagPlugin(), ['MyTag', 'Prefix']);
Instance (scoped) registration — use when you want plugins isolated per engine (e.g. a server handling multiple tenants with different tag sets):
import { TemplateDX } from '@agentmark-ai/templatedx';

const engine = new TemplateDX({ includeBuiltins: true });
engine.registerTagPlugin(new MyTagPlugin(), ['MyTag']);

const rendered = await engine.transform(ast, { /* props */ });
new TemplateDX({ includeBuiltins: true }) copies the static built-ins (If, ElseIf, Else, ForEach, Raw) into the instance; pass false to start empty.

Use the tag in a template

<MyTag prefix="// ">
  This is some content
  that will be prefixed
</MyTag>

Example: Quote tag

import { Node, Root } from 'mdast';
import { TagPlugin, PluginContext, TagPluginRegistry } from '@agentmark-ai/templatedx';

interface QuoteProps {
  author?: string;
}

class QuotePlugin extends TagPlugin<QuoteProps> {
  async transform(
    props: QuoteProps,
    children: Node[],
    context: PluginContext
  ): Promise<Node[]> {
    const { nodeHelpers } = context;

    const content = nodeHelpers.toMarkdown({
      type: 'root',
      children: children,
    } as Root);

    let result = content
      .split('\n')
      .map(line => '> ' + line)
      .join('\n');

    if (props.author) {
      result += `\n> \n> -- ${props.author}`;
    }

    return [{
      type: 'text',
      value: result,
    } as Node];
  }
}

TagPluginRegistry.register(new QuotePlugin(), ['Quote']);
Usage:
<Quote author="Albert Einstein">
  Imagination is more important than knowledge.
</Quote>
Output:
> Imagination is more important than knowledge.
>
> -- Albert Einstein

Creating custom tags (Python)

agentmark-templatedx (Python) mirrors the TS surface. Subclass TagPlugin, implement async def transform, and register via the static (register_global) or instance API.
from templatedx import TagPlugin, TagPluginRegistry
from templatedx.tag_plugin import PluginContext

class QuotePlugin(TagPlugin):
    async def transform(self, props, children, context: PluginContext):
        content = context.node_helpers.to_markdown(children)
        author = props.get("author")
        body = "\n".join(f"> {line}" for line in content.split("\n"))
        if author:
            body += f"\n> \n> -- {author}"
        return [{"type": "text", "value": body}]

# Global (static) registration
TagPluginRegistry.register_global(QuotePlugin(), ["Quote"])
The Python PluginContext dataclass has node_helpers (snake_case mirror of the TS NodeHelpers surface), create_node_transformer, scope, and tag_name. For instance-scoped registration, construct the engine and use register_tag_plugin:
from templatedx import TemplateDX

engine = TemplateDX()  # always copies global built-ins on init
engine.register_tag_plugin(QuotePlugin(), ["Quote"])

result = await engine.transform(ast, {"name": "Alice"})

Built-in tags

ForEach

The ForEach tag loops over an array. Syntax
<ForEach arr={props.arr}>
  {(item, index) => ...}
</ForEach>
Parameters
  • arr: Array<T> — an array of items you want to iterate on
  • children: (item: T, index: number) => Node — a callback function for each item
Example
<ForEach arr={[1, 2]}>
  {(item, index) => (
    <>
      * item: {item}, index: {index}
    </>
  )}
</ForEach>
Output
* item: 1, index: 0
* item: 2, index: 1

Conditionals

The If, ElseIf, and Else tags let you conditionally output content. Syntax
<If condition={props.boolA}>
  ...
</If>
<ElseIf condition={props.boolB}>
  ...
</ElseIf>
<Else>
 ...
</Else>
Parameters If / ElseIf:
  • condition: boolean — the condition to check
  • children: Node — the node to render if the condition is true
Else:
  • children: Node — the content to render if no previous condition was met
Example
<If condition={1 + 1 == 3}>
  1 + 1 is not 3
</If>
<ElseIf condition={1 + 1 == 2}>
  1 + 1 is 2
</ElseIf>
<Else>
  Fallback
</Else>
Output
1 + 1 is 2

Raw

The Raw tag outputs its children verbatim, without expression interpolation. Syntax
<Raw>
  ...
</Raw>
Parameters
  • children: Node — the raw text
Example
<Raw>
  {props.name}
</Raw>
Output
{props.name}