Recipes and templates — recording, replaying and reusing diagrams#
A recipe is a block diagram written as text: one statement per line, in the same
language you type into the command window (The command window — the command engine for a user) — block(...), .move,
.setConfig, connect(...). The application can write a recipe from any diagram and
rebuild the diagram from a recipe, and it uses that pair for three things you meet as a
user: your project file, undo/redo, and templates. A template is a recipe that has
been shipped with the application or saved by you so it can be dropped into any diagram
again. There are 49 built-in templates in this build (42 subsystem templates, listed on
Template catalog — every .icore, plus 7 example projects; measured 2026-09-26).
What a recipe records#
Read a recipe top to bottom and you have the order the application recreates things in
(src/ICoreBlocks/ICoreCoder/ICoreCommandSystem/ICoreRecipeSerializer.cpp):
- Global definitions first, only when the recipe is of the whole project: the project's
enumeration types (
defineEnum(Name, {A, B}, [1 2], Default), below), then the global variables (name = valuelines). - Every block, in the order they sit in the level:
h = block(Type)binds a handle, then.rename,.move(x, y),.resize(w, h),.rotateif rotated,.commentOutif commented out, one.setConfig(key, value)per non-private parameter, and the block's editable ports (clearPorts()thenaddPort(...)). Scope blocks also get their chart settings (buffer width, axis labels, fonts, line styles) — never the plotted data. - Subsystems the same way, with
h = subsystem(); the recipe then descends into the subsystem's contents before continuing. A subsystem's own ports are not written — they reappear as the gate blocks inside it are recreated. - Links after all blocks of a level:
l = connect(a<0>, b<0>), the link's name, and its route asl.setCorners((x, y), ...). A branch off an existing link is a furtherconnect(l, c<0>). - Areas, images and text boxes last, with position, size, colour, title and text.
- Two timestamp lines (
subsystemTimes(...),h.setTimes(...)) — created and last-modified times, as integers.
So geometry is recorded: a replayed diagram places every block at the recorded
coordinates and routes every link through the recorded corners. Coordinates are relative
to the level's origin anchor, with y increasing upward. Handles (Step, l_2, …) are
minted from the live names and only matter inside the recipe; the visible name is what
.rename(...) sets. Every generated statement ends in ;, which is the console's
"do not echo" marker, so a replay prints nothing unless a line fails.
This is what a shipped template looks like
(src/ICoreBlocks/ICoreBlockLibrary/Templates/Subsystems/first_order_lag.icore, trimmed):
// @title: First-Order Lag
// @category: Control Systems
// @summary: A step into a 1/(s+1) transfer function, watched on a scope.
// @kind: subsystem
src = block(Step)
src.move(40, 295)
src.setConfig(Final Value, 1)
plant = block(Transfer_Function)
plant.move(240, 305)
plant.resize(130, 90)
plant.setConfig(Numerator, [1])
plant.setConfig(Denominator, [1, 1])
out = block(Scope)
out.move(460, 295)
connect(src<0>, plant<0>)
connect(plant<0>, out<0>)
The // @title / @category / @summary / @kind comment lines are the template's metadata;
the file describes itself, there is no separate index. A subsystem template may also carry
// @face: <setting> = <value> lines, which set the inserted subsystem block itself (its
Treat as Atomic Unit, say) rather than a block inside it. Whole-line // and # comments
are skipped on replay; a hand-written recipe may leave the trailing ; off.
The rules a user meets#
- Your project file is a recipe.
<Project>/<Project>.iprojis plain recipe text of the whole project (ICoreStudioSerialization.h); solver settings and export targets travel beside it assolver.iniandexportTargets.ini, not inside the recipe. Opening a project empties the diagram and replays the file; if a line is not understood by this version, loading stops there and the project is reported as failed rather than saved back half-rebuilt. - Each subsystem has its own undo and redo (since 2026-09-23). Ctrl+Z undoes the last
change made in the subsystem the canvas you are looking at shows — not the last change
anywhere in the project; with no canvas focused it acts on Home. A change that touched two
levels at once (creating a subsystem, adding or removing a gate port, deleting a subsystem)
is one step in both, and it is refused — with a message naming the other level — while
that other level has newer changes of its own: undo those first. Undo and redo are still
recipe replays underneath (
ICoreStudioStateMachine.h), and scope settings (axis labels, colours) come back as they were at that step. - A subsystem can live in a file of its own. Give a Subsystem a Referenced File in its
settings, a path inside the project folder such as
References/Filter.icore, and it becomes an instance of that file: the project file keeps the path and the subsystem's ports, and the file keeps what is inside. Every instance of one file shows that file's contents when the project opens. Editing an instance edits the file. The first instance you edit holds the file until you save, and saving writes the file first, then the project. Until then, an edit in another instance of the same file is refused and taken back with a message naming the instance that holds it, and so is an undo or redo there. Saving also creates the file from the instance when it is not there yet. A file that is missing when the project opens is reported, and the instance keeps its ports and links with nothing inside; the project still opens. - Importing a recipe or inserting a template APPENDS. Nothing on the level is
removed; the recipe's top-level objects are re-parented into the level on screen. The
whole insert is one undo step (
ICoreRecipeFileTransfer.h,ICoreTemplateLibrary.h). - A template inserts as one subsystem, named after its title; if that name is already
taken in the level you get a numbered sibling (
First-Order Lag 2), never a refusal and never a merge (uniqueSiblingNameinICoreTemplateLibrary.cpp, run 5 below). - A failing line does not stop a replay. Each failed statement becomes one warning
(
! '…' failed: …) under an otherwise successful "inserted/imported" message; read them — a block that is missing from the result is reported there and nowhere else. - A block name must be unique within its level.
rename()refuses a taken name (rename(): a block named 'X' already exists in …) — the usual cause is replaying a recipe into a level that still holds the originals. - Your own templates never overwrite silently. From the console,
saveTemplaterefuses a name that exists; the toolbar dialog asksReplace Template?first. - Built-in templates are read-only and refreshed at every launch. The application
wipes and rewrites its on-disk copy of the shipped set from the binary each start
(
Initialization.cpp,mirrorBuiltInTemplates), so editing a file in the.builtinsfolder is lost at the next launch. Save your own copy instead. - An enumeration type is a project definition.
defineEnum(BfMode, {Off, On, Fault}, [0 1 9], Off)defines a type the way MATLAB'sSimulink.defineIntEnumType('BfMode', {'Off','On','Fault'}, [0 1 9], 'DefaultValue', 'Off')does, with the same rules: names are identifiers and appear once, values are whole numbers that fit an int32, a value two names share makes the second an alias, and without a default the first member is the default. Defining the type again replaces it. It is saved with the project and comes back when the project opens; one statement is one undo step.
Things that surprise users#
- "I inserted a template and it landed on top of my blocks." A template is placed at
the position you give (
useTemplate <id> [parent] [x] [y], default the origin) or, from the toolbar, at the centre of the visible canvas; its interior keeps the authored coordinates. Move the new subsystem — its contents come with it. - "The imported diagram is there but every link is dangling / a block is missing." Scroll the notification: the per-line warnings name the statement that failed. Typical cause: the recipe names a block type this build does not have, or a port index that does not exist on it.
- "My template shows no summary and sits under
Uncategorized." Metadata is read from the// @…header lines at the top of the file. A hand-written file without them lists under its file stem, categoryUncategorized. - "
k = block(Gain) // notedid nothing, and every laterk.movefails too." Only whole-line comments are skipped; a trailing comment is handed to the interpreter as part of the statement, the handle never binds, and every later use of it fails. Put comments on their own line. - "I edited an example project and my changes are in the template." They are not: an
example opens as a copy under your projects folder (
Documents/ICore Blocks/<Name>/); the shipped copy is untouched. Once you save, the copy has lost its// @titleheader and is an ordinary project (observed in run 6). - "I inserted one of my own templates and my solver settings changed." A template you
saved yourself carries the solver settings of the model it was saved from, as
setModelConfiglines ahead of its blocks, and inserting it replays them into the model you insert it into (run 7 below). The shipped templates carry none. CheckmodelConfigafter inserting one of yours, or delete those lines from its file. - "Where do saved scripts run?" The
Code Engine → ICore Script IDEwindow (Run,Run All,New,Save,Delete) keeps scripts under<Project>/scripts/*.icoreand stops at the first failing line; itsImport Diagrambutton writes the recipe of any subsystem into a new script so you can start from something already drawn (ICoreScriptRunner.h,ICoreCommandScriptWindow.cpp).
Where the surfaces are#
| You want to | Toolbar / menu | Console (see Command glossary — console commands, verbs, functions) |
|---|---|---|
| list templates | Templates button → Insert Template (by category); Library Navigator → Subsystem Templates; the New Tab card → SUBSYSTEM TEMPLATES | templates [category or kind] |
| insert a template as a subsystem | Templates → Insert Template → category → title; or drag from the Library Navigator | useTemplate <id> [parent] [x] [y] |
| start a project from a template | New Project form → PROJECT TEMPLATES cards (or Blank Project) | openTemplate <id> [name] |
| save the current level as a template | Templates → Save '<level>' as Template… (title, category, summary) | saveTemplate <name> (Home, category My Templates) |
| find your template files | Templates → Open Templates Folder… | path printed by saveTemplate |
| write a level to a recipe file | Export → Export as ICore Recipe... (*.icore) | generateRecipe [path] prints it |
| replay a recipe file into the level on screen | Import → Import as ICore Recipe... | paste the lines into the command window, or ICore Script IDE |
Template ids are stable addresses: subsystems/<stem> and examples/<Folder> for the
built-ins, user/subsystems/<stem> for yours; ids are matched case-insensitively.
Your own templates live in the application's data folder, not with your projects:
~/Library/Application Support/ICore Blocks/Templates/ on macOS,
%LOCALAPPDATA%\ICore Blocks\Templates\ on Windows,
~/.local/share/ICore Blocks/Templates/ on Linux (ICoreDocumentsFolder.cpp,
ICoreStandardPaths.cpp). Drop a .icore into Templates/Subsystems/ (or a project
folder into Templates/Examples/<Name>/; create the folder if it is not there yet) and it is in every list on the next look — the
catalog rescans the folder each time it is opened, no restart needed. The shipped set is
mirrored into Templates/.builtins/ (hidden) beside them.
How it works, briefly#
A recipe line and a console line are the same thing — the interpreter tries the recipe
grammar before the console's math grammar. useTemplate builds one text (a subsystem()
statement, a move, then the template body with its top-level parents rewritten to the
new subsystem) and replays it as one stream, which is why the insert is one undo step.
saveTemplate and Export as ICore Recipe... use the same writer as the project file
plus a // ICoreBlocks recipe (source: …) header that tells a later import which
parents to rewrite. Export as ICore Recipe... also writes the solver configuration ahead
of the blocks, one setModelConfig <property> <value>; line per setting (since 2026-09-03),
and an import applies those lines through the console's own command, so the file carries
the run's settings as well as the diagram; a bad line is reported and the rest still lands.
A project keeps its solver settings in solver.ini instead, and a shipped subsystem template
carries none, which is why an example that needs a configured solver ships as a project folder
(.iproj + solver.ini). A template you save yourself is written by the same writer as
Export as ICore Recipe..., so it does carry the setModelConfig lines, and inserting it
applies them.
The Simulink script does the same in Simulink's words — see the Simulink bridge entry of
User glossary — the vocabulary of using ICoreBlocks.
Real runs#
Headless, on the build of 2026-09-25 23:58 (approximately commit 1dac24c5), with a
private HOME so nothing touches real projects; re-run 2026-09-26. Startup lines stripped
and the scratch path shortened to <scratch>; everything else verbatim.
(a) List, insert, and read the level back as a recipe — the read-back is what
generateRecipe (or Export as ICore Recipe...) produces from a live diagram:
$ HOME=<scratch> ICoreBlocks.app/Contents/MacOS/ICoreBlocks --console "templates" | grep "template(s)"
49 template(s):
$ HOME=<scratch> ICoreBlocks.app/Contents/MacOS/ICoreBlocks \
--console "useTemplate subsystems/first_order_lag Home 40 40"
Software Initialized
inserted 'First-Order Lag' into Home as subsystem 'First-Order Lag' — 4 block(s), 2 connection(s)
exit=0
$ HOME=<scratch> ICoreBlocks.app/Contents/MacOS/ICoreBlocks \
--console "useTemplate subsystems/first_order_lag Home 40 40; generateRecipe Home"
Software Initialized
First_Order_Lag = subsystem();
First_Order_Lag.rename(First-Order Lag);
First_Order_Lag.move(40, 40);
First_Order_Lag.resize(160, 120);
First_Order_Lag.setConfig(Sampling Time (s), -1);
Step = block(Control_Systems/Sources/Step, First_Order_Lag);
Step.rename(Step);
Step.move(40, 295);
Step.resize(70, 70);
Step.setConfig(Sampling Time (s), -1);
Step.setConfig(Step Time (sec), 1);
Step.setConfig(Initial Value, 0);
Step.setConfig(Final Value, 1);
...
l = connect(Step<0>, Transfer_Function<0>);
l.rename("ICoreDouble");
l.setCorners((105, 260), (245, 260));
l_2 = connect(Transfer_Function<0>, Scope<0>);
l_2.rename("ICoreDouble 1");
l_2.setCorners((365, 260), (465, 260));
subsystemTimes(ICore Blocks/Home, 1790448651635, 1790448653962);
First_Order_Lag.setTimes(1790448653908, 1790448653962);
exit=0
Note the "4 block(s)": the count includes the subsystem itself. Note also that the
read-back spells full block types (Control_Systems/Sources/Step) where the shipped
file wrote Step; both replay.
(b) The mirrored built-in templates after one launch:
$ cd "<scratch>/Library/Application Support/ICore Blocks" && ls -a Templates
. .. .builtins
$ ls Templates/.builtins/Subsystems | wc -l ; ls Templates/.builtins/Examples
42
Closed_Loop_Speed_Control DC_Motor_Drivetrain Digital_PID_Loop Kalman_Tracking_Loop
Mobile_Base_Steering Recursive_Identification_Rig Two_Degree_PID_Loop
(c) Build a small diagram from recipe lines, save it as a template, and see it list — the file it produced is the recipe the diagram serializes to, after the solver settings it carries (excerpt of its 44 lines):
$ HOME=<scratch> ICoreBlocks.app/Contents/MacOS/ICoreBlocks --console \
"g = block(Gain); g.setConfig(Gain Value, 2.5); g.move(100, 200); s = block(Sine_Wave); s.move(0, 200); connect(s<0>, g<0>); saveTemplate My Gain Stage"
Software Initialized
saved 'My Gain Stage' as a subsystem template:
<scratch>/Library/Application Support/ICore Blocks/Templates/Subsystems/My_Gain_Stage.icore
insert it with: useTemplate user/subsystems/My_Gain_Stage
$ head ".../Templates/Subsystems/My_Gain_Stage.icore"
// ICoreBlocks recipe (source: ICore Blocks/Home)
// @title: My Gain Stage
// @category: My Templates
// @kind: subsystem
// ---- Solver configuration (modelConfig) ----
setModelConfig startTime 0;
setModelConfig stopTime 10;
setModelConfig infiniteSimulation false;
setModelConfig maximumConsecutiveTimeBuffer 10000;
setModelConfig slowPaceStepDelay 0;
$ HOME=<scratch> ICoreBlocks.app/Contents/MacOS/ICoreBlocks --console "templates My Templates"
1 template(s):
My Templates
user/subsystems/My_Gain_Stage My Gain Stage (yours)
useTemplate <id> [parent] [x] [y] insert as a subsystem (default Home)
openTemplate <id> [name] open as a new project
(d) A name clash on insert, opening an example as a project, and a saved template's solver settings arriving with it (runs 5, 6 and 7):
$ ... --console "useTemplate subsystems/first_order_lag; useTemplate subsystems/first_order_lag Home 300 40"
inserted 'First-Order Lag' into Home as subsystem 'First-Order Lag 2' — 4 block(s), 2 connection(s)
$ ... --console "openTemplate examples/Digital_PID_Loop My PID Loop"
[ICoreRunDiagnosis] Loading config at block: ICore Blocks/Home/Discrete PID Controller
[ICoreRunDiagnosis] Loading config at block: ICore Blocks/Home/Discrete PID Controller 2DOF
opened 'Digital PID Loop' as a new project:
<scratch>/Documents/ICore Blocks/My PID Loop
$ ls "<scratch>/Documents/ICore Blocks/My PID Loop"
AGENTS.md CLAUDE.md GEMINI.md Images My PID Loop.iproj exportTargets.ini
projectPreferences.ini solver.ini
$ ... --console "setModelConfig stopTime 5; g = block(Gain); saveTemplate Five"
saved 'Five' as a subsystem template:
<scratch>/Library/Application Support/ICore Blocks/Templates/Subsystems/Five.icore
insert it with: useTemplate user/subsystems/Five
$ ... --console "setModelConfig stopTime 20; useTemplate user/subsystems/Five; getModelConfig stopTime"
5
For contributors#
Authoring geometry, the replay traps behind the warnings above and the rebuild-free
verification loop are on the contributor page for adding a template; the full recipe
grammar and the serializer's permanent round-trip limits are on the ICoreCoder module
page (both in this page's related:).