Appearance
Template Language Reference
The complete syntax of the ErgoPlanner template DSL — every
param.*constructor and everyctx.*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… */ },
});| Field | Type | Purpose |
|---|---|---|
name | string | Human-readable name shown in the gallery / stencil |
level | 0 | 1 | 2 | 3 | Hierarchy tier (0 = sub-assembly, 1 = process unit, 2 = area, 3 = works) |
category | string | Slash-separated category path for the stencil tree |
standard | string | Drawing standard (e.g. BS 1553, ISO 10628) |
params | ParamSchema | The configurable inputs (see §3) |
interfaces | (p) => Record<string, InterfacePoint> | Connection boundaries (see §4) |
build | (p, ctx) => void | Places 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. xincreases to the east (right);yincreases 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.
| Constructor | Signature | Runtime value | UI control |
|---|---|---|---|
param.dimension | (default: string, opts?: { min?, max? }) | string | DN dropdown |
param.enum | (default: T, options: readonly T[]) | T (literal union) | dropdown |
param.boolean | (default: boolean) | boolean | toggle |
param.integer | (default: number, opts?: { min?, max? }) | number | stepper / slider |
param.number | (default: number, opts?: { min?, max?, unit? }) | number | number input + unit |
param.string | (default: string) | string | text input |
param.symbol | (default: string, opts?: { category?, tags? }) | string | symbol 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 conston enums. Writingparam.enum('DI', ['DI','SS316'] as const)narrowsp.pipe_materialto'DI' | 'SS316', soctx.when(p.pipe_material === 'SS316', …)is type-checked.
ParamValues mapping
Inside interfaces and build, p is the resolved value map:
| Definition | p.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
}): PlacedSymboltypescript
// 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, notlabel. Equipment identifiers use thetagoption. (Free-floating text usesctx.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 }): PipeRuntypescript
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][]): voidtypescript
ctx.route(main, [[0, 0], [18, 0]]); // straight run
ctx.route(header, [[16, 19], [24, 19], [30, 10], [36, 10]]); // multi-segmentctx.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' }
}): BranchGrouptypescript
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): PipeRuntypescript
// 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');
connecttakes exactly two arguments — there is no third options object. Choose the line type via the ports / interfacetype.
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] }): ComposedTemplatetypescript
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
| Method | Signature | Purpose |
|---|---|---|
ctx.group | group(id: string, children: unknown[]): Group | Group elements for selection/movement |
ctx.label | label(text: string, opts: { at; style?; rotation? }): Label | Free-floating text |
ctx.note | note(text: string, opts: { at; format? }): NoteRef | Numbered/lettered drawing note |
ctx.boundary | boundary(opts: { at; width; height; type?; style?; label? }): Boundary | Chamber / 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): voidtypescript
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): voidtypescript
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 by | Shape (key fields) |
|---|---|
place → PlacedSymbol | { symbolId, position, tag?, interfaces } |
pipe → PipeRun | { id, size?, material? } |
branch → BranchGroup | { direction, pipe, symbols[] } |
connect → PipeRun | the auto-routed connecting pipe |
compose → ComposedTemplate | { templateId, position, interfaces } |
label → Label · boundary → Boundary · note → NoteRef · group → Group | positioned 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 | ballDrain / 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
- [ ]
slugis unique and kebab-case;name,level,category,standardset - [ ] Every parameter has a sensible default; numeric/integer params have
min/max; enums useas const - [ ]
interfacescover every external connection (withdirection,size,type) - [ ]
buildusestagfor equipment,ctx.labelfor free text - [ ] Optional content is wrapped in
ctx.when; repeats usectx.repeat - [ ] All 10 discipline prompts are filled (incl. DSEAR/HAZOP)
- [ ] Parameter tester exercised at
minandmaxof each numeric param - [ ] Quality gate run; reconstruction score ≥ 85 before
published
Related topics
- Drawing Templates Overview
- Pumping Stations · Instrumentation Arrangements
- Developer tutorial:
docs/06-specifications/mbep/50-creating-first-template.md