Skip to content

Template Language Reference ​

The complete syntax of the ErgoPlanner template DSL — every param.* constructor and every ctx.* build verb, with examples

This page is the authoritative reference for authoring drawing templates in @ergoplanner/template-sdk. Every signature here matches the shipped SDK (packages/web/packages/template-sdk/src/). For a gentler introduction see the Overview; for end-to-end worked use cases see Pumping Stations and Instrumentation Arrangements.


1. Template anatomy ​

A template is a single default-exported call to Template.define(slug, definition):

typescript
import { Template, param } from '@ergoplanner/template-sdk';

export default Template.define('my-template-slug', {
  name: 'My Template',                 // display name in the stencil
  level: 0,                            // 0 | 1 | 2 | 3  (hierarchy tier)
  category: 'Common Arrangements/x',   // slash-separated stencil path
  standard: 'BS 1553 / BS 1646',       // engineering standard followed
  params: { /* …param.* definitions… */ },
  interfaces: (p) => ({ /* …connection points… */ }),
  build: (p, ctx) => { /* …place symbols & route pipes… */ },
});
FieldTypePurpose
namestringHuman-readable name shown in the gallery / stencil
level0 | 1 | 2 | 3Hierarchy tier (0 = sub-assembly, 1 = process unit, 2 = area, 3 = works)
categorystringSlash-separated category path for the stencil tree
standardstringDrawing standard (e.g. BS 1553, ISO 10628)
paramsParamSchemaThe configurable inputs (see §3)
interfaces(p) => Record<string, InterfacePoint>Connection boundaries (see §4)
build(p, ctx) => voidPlaces symbols / pipes from parameter values (see §5)

Both interfaces and build receive p — the resolved parameter values (not the definitions). param.integer(3,…) becomes a plain number, param.enum('DI',…) becomes the literal union 'DI' | …, etc. (see §3, ParamValues).

The discipline AI prompts are exported alongside the template as a prompts object (export const prompts = { layout, processDescription, … }) — see Overview.


2. Coordinate system ​

  • Positions are [x, y] in grid units (GU). 1 GU = 8 mm in model space.
  • x increases to the east (right); y increases to the south (down).
  • The template origin is [0, 0]; everything is placed relative to it.
  • Rotation is in degrees, restricted to 0 | 90 | 180 | 270.
  • Direction (for interfaces and branches) is one of 'north' | 'south' | 'east' | 'west'.

3. Parameters — the param.* constructors ​

Parameters are declared in the params map. Each constructor returns a frozen definition object with a type discriminator and drives a specific UI control in the parameter tester.

ConstructorSignatureRuntime valueUI control
param.dimension(default: string, opts?: { min?, max? })stringDN dropdown
param.enum(default: T, options: readonly T[])T (literal union)dropdown
param.boolean(default: boolean)booleantoggle
param.integer(default: number, opts?: { min?, max? })numberstepper / slider
param.number(default: number, opts?: { min?, max?, unit? })numbernumber input + unit
param.string(default: string)stringtext input
param.symbol(default: string, opts?: { category?, tags? })stringsymbol picker

Examples ​

typescript
params: {
  // Pipe / equipment size in DN notation, with min/max
  pipe_size:      param.dimension('DN150', { min: 'DN50', max: 'DN600' }),

  // Fixed choices — the default MUST be one of the options; the union is inferred.
  // `as const` keeps the literal type narrow (recommended).
  pipe_material:  param.enum('DI', ['DI', 'uPVC', 'SS316', 'HDPE'] as const),

  // Toggle for optional content
  include_bauer:  param.boolean(true),

  // Whole number with bounds (drives ctx.repeat counts)
  pump_count:     param.integer(3, { min: 1, max: 6 }),

  // Decimal with engineering unit
  design_flow:    param.number(42.5, { min: 0, max: 2000, unit: 'L/s' }),

  // Free text — typically the tag prefix
  tag_prefix:     param.string('IPS01'),

  // Symbol chosen from the shape library, filtered by category
  motor:          param.symbol('fixed-speed-motor', { category: 'prime-movers' }),
}

Tip — as const on enums. Writing param.enum('DI', ['DI','SS316'] as const) narrows p.pipe_material to 'DI' | 'SS316', so ctx.when(p.pipe_material === 'SS316', …) is type-checked.

ParamValues mapping ​

Inside interfaces and build, p is the resolved value map:

Definitionp.x type
param.dimension(…)string
param.enum('DI', [...] as const)'DI' | …
param.boolean(…)boolean
param.integer(…) / param.number(…)number
param.string(…) / param.symbol(…)string

4. Interface points ​

interfaces(p) returns a record of named connection boundaries — where external pipes (or parent templates) attach. Each is an InterfacePoint:

typescript
interface InterfacePoint {
  position: [number, number];                  // [x, y] in GU, relative to origin
  direction: 'north' | 'south' | 'east' | 'west';  // faces outward
  size?: string;                               // e.g. 'DN150'
  type?: string;                               // 'pipe' | 'signal-4-20mA' | 'vessel-mount' | …
}
typescript
interfaces: (p) => ({
  inlet:  { position: [0, 0],  direction: 'west', size: p.pipe_size, type: 'pipe' },
  outlet: { position: [18, 0], direction: 'east', size: p.pipe_size, type: 'pipe' },
}),

Interface points can be parametric — generate one per repeated element:

typescript
interfaces: (p) => {
  const points: Record<string, InterfacePoint> = {};
  for (let i = 0; i < p.pump_count; i++) {
    points[`branch_${i + 1}`] = { position: [i * 4, 6], direction: 'south', size: p.branch_size, type: 'pipe' };
  }
  points.header_out = { position: [(p.pump_count - 1) * 4, 0], direction: 'east', size: p.header_size, type: 'pipe' };
  return points;
},

5. The build context — ctx.* ​

build(p, ctx) is declarative: it describes what to place, not how to render it. The ctx object exposes 12 methods in four groups.

5.1 Placement ​

ctx.place(symbolId, opts) → PlacedSymbol ​

Place a symbol from the registry.

typescript
place(symbolId: string, opts: {
  at: [number, number];        // required position in GU
  on?: PipeRun;                // place inline ON a pipe run (e.g. a valve on the main)
  tag?: string;                // equipment tag, e.g. `${p.tag_prefix}V_01`
  rotation?: number;           // 0 | 90 | 180 | 270
  width?: number; height?: number;  // pixel overrides (default = shape library)
  props?: Record<string, unknown>;  // symbol props (size, material, output, …)
  signalTo?: PlacedSymbol;     // draw a signal line to another placed symbol
}): PlacedSymbol
typescript
// An isolation valve placed inline on the main pipe run, fully tagged
const v1 = ctx.place(`valve-${p.iso_valve_type}-standard`, {
  at: [3, 0],
  on: main,
  tag: `${p.tag_prefix}V_01`,
  props: { size: p.pipe_size, material: p.pipe_material },
});

tag, not label. Equipment identifiers use the tag option. (Free-floating text uses ctx.label() — see §5.3.)

The returned PlacedSymbol has { symbolId, position, tag?, interfaces } — pass it to ctx.connect() or another place's signalTo.

ctx.pipe(id, opts) → PipeRun ​

Declare a pipe run (then lay it out with route).

typescript
pipe(id: string, opts: { size?: string; material?: string; lineType?: string; spec?: string }): PipeRun
typescript
const main = ctx.pipe('main', { size: p.pipe_size, material: p.pipe_material });

ctx.route(pipe, waypoints) → void ​

Route a declared pipe through [x, y] waypoints.

typescript
route(pipe: PipeRun, waypoints: [number, number][]): void
typescript
ctx.route(main, [[0, 0], [18, 0]]);          // straight run
ctx.route(header, [[16, 19], [24, 19], [30, 10], [36, 10]]);  // multi-segment

ctx.branch(pipe, opts) → BranchGroup ​

Tee a branch off a pipe — for drains, sample points, bypasses.

typescript
branch(pipe: PipeRun, opts: {
  at: [number, number];                       // tee position on the parent pipe
  direction: 'north' | 'south' | 'east' | 'west';
  size?: string;                              // branch size (defaults to parent)
  components?: { symbol: string; tag?: string; offset: [number, number]; props?: object }[];
  terminator?: { label?: string; symbol?: string };  // e.g. { label: 'DRAIN' }
}): BranchGroup
typescript
ctx.branch(main, {
  at: [13, 0],
  direction: 'south',
  size: p.drain_size,
  components: [
    { symbol: 'valve-gate-standard', tag: `${p.tag_prefix}V_04`, offset: [0, 2], props: { size: p.drain_size } },
  ],
  terminator: { label: 'DRAIN' },
});

ctx.connect(from, to) → PipeRun ​

Connect two interface points with an auto-routed pipe. Each end is an InterfaceRef — a string path ("pump.discharge"), an InterfacePoint, or { symbol: PlacedSymbol, port: string }.

typescript
connect(from: InterfaceRef, to: InterfaceRef): PipeRun
typescript
// Signal line between a flow element and its transmitter
ctx.connect({ symbol: flowElement, port: 'signal_out' }, { symbol: flowTransmitter, port: 'signal_in' });

// Connect a placed pump's discharge to a composed manifold's branch interface
ctx.connect({ symbol: pump, port: 'discharge' }, 'manifold.branch_1');

connect takes exactly two arguments — there is no third options object. Choose the line type via the ports / interface type.

5.2 Composition ​

ctx.compose(templateId, params, pos) → ComposedTemplate ​

Embed another template (nesting) and wire to it through its interface points.

typescript
compose(templateId: string, params: Record<string, unknown>, pos: { at: [number, number] }): ComposedTemplate
typescript
const flowMeter = ctx.compose(
  'flowmeter-compound-inline',
  { pipe_size: p.discharge_size, flowmeter_type: 'electromagnetic', tag_prefix: p.tag_prefix },
  { at: [40, 10] },
);
ctx.connect('risingMain.out', { symbol: flowMeter as unknown as PlacedSymbol, port: 'inlet' });

Note the three arguments: (templateId, paramsObject, { at }). Max nesting depth is 2 levels.

5.3 Organisation ​

MethodSignaturePurpose
ctx.groupgroup(id: string, children: unknown[]): GroupGroup elements for selection/movement
ctx.labellabel(text: string, opts: { at; style?; rotation? }): LabelFree-floating text
ctx.notenote(text: string, opts: { at; format? }): NoteRefNumbered/lettered drawing note
ctx.boundaryboundary(opts: { at; width; height; type?; style?; label? }): BoundaryChamber / skid / package outline
typescript
ctx.label(`${p.tag_prefix}FM_01`, { at: [9, 3], style: 'tag' });

ctx.boundary({ at: [-1, -4], width: 20, height: 12, type: 'skid', style: 'dashed', label: 'DOSING SKID' });

5.4 Control flow ​

ctx.when(condition, fn) → void ​

Conditional content — the workhorse for optional components.

typescript
when(condition: boolean, fn: () => void): void
typescript
ctx.when(p.include_drain_ds, () => {
  ctx.branch(main, { at: [13, 0], direction: 'south', /* … */ });
});

ctx.repeat(count, fn) → void ​

Loop — the callback receives the zero-based index.

typescript
repeat(count: number, fn: (index: number) => void): void
typescript
ctx.repeat(p.pump_count, (i) => {
  ctx.place('pump-submersible', {
    at: [4 + i * 6, 12],
    tag: `${p.tag_prefix}P${i + 1}`,
  });
});

6. Return objects (quick reference) ​

Returned byShape (key fields)
place → PlacedSymbol{ symbolId, position, tag?, interfaces }
pipe → PipeRun{ id, size?, material? }
branch → BranchGroup{ direction, pipe, symbols[] }
connect → PipeRunthe auto-routed connecting pipe
compose → ComposedTemplate{ templateId, position, interfaces }
label → Label · boundary → Boundary · note → NoteRef · group → Grouppositioned drawing objects

7. Common patterns ​

Optional component — wrap in ctx.when:

typescript
ctx.when(p.include_iso, () => ctx.place('valve-gate-standard', { at: [0, 2], on: riser, tag: `${p.tag_prefix}ISO` }));

Repeated array — ctx.repeat, offsetting by index:

typescript
ctx.repeat(p.pump_count, (i) => ctx.place('pump-submersible', { at: [i * 6, 0], tag: `${p.tag_prefix}P${i + 1}` }));

Inline device on a pipe — pass the PipeRun as on:

typescript
const main = ctx.pipe('main', { size: p.pipe_size });
ctx.route(main, [[0, 0], [18, 0]]);
ctx.place('instrument-flow-element', { at: [9, 0], on: main, tag: `${p.tag_prefix}FE_01` });

Instrument signal — element + transmitter joined by connect on signal ports:

typescript
const fe  = ctx.place('instrument-flow-element',       { at: [9, 0],  on: main, tag: `${p.tag_prefix}FE_01` });
const fit = ctx.place('instrument-transmitter-circle', { at: [9, -3],          tag: `${p.tag_prefix}FIT_01`,
  props: { function_letter: 'FIT', output: '4-20mA HART' } });
ctx.connect({ symbol: fe, port: 'signal_out' }, { symbol: fit, port: 'signal_in' });

Valve family switch — interpolate the symbol id from an enum:

typescript
ctx.place(`valve-${p.iso_valve_type}-standard`, { at: [3, 0], on: main });  // gate | butterfly | ball

Drain / sample tee with a label — ctx.branch + terminator:

typescript
ctx.branch(main, { at: [6, 0], direction: 'south', size: p.drain_size,
  components: [{ symbol: 'valve-gate-standard', offset: [0, 2], props: { size: p.drain_size } }],
  terminator: { label: 'DRAIN' } });

8. Complete example (Level 0) ​

A faithful, compiling shape mirroring the shipped flowmeter-compound-inline template:

typescript
import { Template, param } from '@ergoplanner/template-sdk';

export const prompts = {
  layout: 'Horizontal main, west→east. Isolation valves up/downstream of an inline flow element; transmitter bubble above.',
  processDescription: 'Inline flow metering compound with isolation and optional drains.',
  // …remaining discipline prompts…
};

export default Template.define('flowmeter-compound-inline', {
  name: 'Inline Flowmeter Compound',
  level: 0,
  category: 'Common Arrangements/flow-measurement-inline',
  standard: 'BS 1553 / BS 1646',

  params: {
    pipe_size:      param.dimension('DN150', { min: 'DN50', max: 'DN600' }),
    pipe_material:  param.enum('DI', ['DI', 'uPVC', 'SS316', 'HDPE'] as const),
    flowmeter_type: param.enum('electromagnetic', ['electromagnetic', 'ultrasonic-inline', 'vortex'] as const),
    include_drain_ds: param.boolean(true),
    drain_size:     param.enum('3/4" BSP', ['1/2" BSP', '3/4" BSP', '1" BSP'] as const),
    iso_valve_type: param.enum('gate', ['gate', 'butterfly', 'ball'] as const),
    tag_prefix:     param.string('IPS01'),
  },

  interfaces: (p) => ({
    inlet:  { position: [0, 0],  direction: 'west', size: p.pipe_size, type: 'pipe' },
    outlet: { position: [18, 0], direction: 'east', size: p.pipe_size, type: 'pipe' },
  }),

  build: (p, ctx) => {
    const main = ctx.pipe('main', { size: p.pipe_size, material: p.pipe_material });
    ctx.route(main, [[0, 0], [18, 0]]);

    ctx.place(`valve-${p.iso_valve_type}-standard`, {
      at: [3, 0], on: main, tag: `${p.tag_prefix}V_01`,
      props: { size: p.pipe_size, material: p.pipe_material },
    });

    const fe  = ctx.place('instrument-flow-element', {
      at: [9, 0], on: main, tag: `${p.tag_prefix}FE_01`,
      props: { size: p.pipe_size, instrument_type: p.flowmeter_type },
    });
    const fit = ctx.place('instrument-transmitter-circle', {
      at: [9, -3], tag: `${p.tag_prefix}FIT_01`,
      props: { function_letter: 'FIT', output: '4-20mA HART' },
    });
    ctx.connect({ symbol: fe, port: 'signal_out' }, { symbol: fit, port: 'signal_in' });

    ctx.place(`valve-${p.iso_valve_type}-standard`, {
      at: [15, 0], on: main, tag: `${p.tag_prefix}V_03`,
      props: { size: p.pipe_size, material: p.pipe_material },
    });

    ctx.when(p.include_drain_ds, () => {
      ctx.branch(main, {
        at: [13, 0], direction: 'south', size: p.drain_size,
        components: [{ symbol: 'valve-gate-standard', tag: `${p.tag_prefix}V_04`, offset: [0, 2], props: { size: p.drain_size } }],
        terminator: { label: 'DRAIN' },
      });
    });

    ctx.label(`${p.tag_prefix}FM_01`, { at: [9, 3], style: 'tag' });
  },
});

9. Authoring checklist ​

  • [ ] slug is unique and kebab-case; name, level, category, standard set
  • [ ] Every parameter has a sensible default; numeric/integer params have min/max; enums use as const
  • [ ] interfaces cover every external connection (with direction, size, type)
  • [ ] build uses tag for equipment, ctx.label for free text
  • [ ] Optional content is wrapped in ctx.when; repeats use ctx.repeat
  • [ ] All 10 discipline prompts are filled (incl. DSEAR/HAZOP)
  • [ ] Parameter tester exercised at min and max of each numeric param
  • [ ] Quality gate run; reconstruction score ≥ 85 before published