TopDo has no templates table. A template is an ordinary node carrying is_template = true, and run_process copies its subtree into real nodes that people and agents pick up like any other work. That is the whole mechanism, which is why the interesting question is not how templates work but what you put inside one. A copy operation adds nothing that was not already there. It repeats your structure exactly, at whatever quality you left it, every time somebody runs it.

A template is a node with a flag on it
There is no separate templates table, no template editor and no template permission. A template is a row in nodes with the boolean column is_template set to true. Everything that applies to a node applies to it unchanged: a type from the registry, a title, a Markdown body, labels, metadata, children beneath it and edges out of it. The flag decides where the node shows up, not what it is allowed to contain.
The process list you see is narrower than the set of templates. listProcesses returns only template roots — rows where is_template is true and there is no live template parent above them — as {id, title, created_at}. A template ten nodes deep is not a process in its own right; it is part of the tree under the root that owns it. That is why the list stays short while the templates themselves can be as deep as the work requires.
This costs you one concept instead of two. There is no second object model to learn, no import and export step between a template and a real task, and no separate audit path. Whatever you already know about nodes, edges and types is what you know about templates.
The flag is inherited, and that is where people get caught
Node creation computes the flag from the parent: a node is a template if you asked for one or if its parent is one. Anything you create under a template is a template, whether or not you meant it. That is usually the behavior you want, because it means you build a process by adding children in the ordinary way rather than by marking each step. It is also the single most common surprise, because nothing in the act of creating a child announces it.
Toggling the flag cascades. Setting is_template on a node walks its subtree and writes the same value to every descendant, including soft-deleted ones. Turning it off also fills in a missing assignee from the workspace default, so nodes that were drafted ownerless inside a template come back out with somebody on them. One update on the root converts a whole tree in either direction.
The move operation is where the asymmetry bites, so read this twice. Moving a node under a template forces is_template = true across the entire moved subtree — and there is no reverse. Moving it back out does not clear the flag. A live task dragged under a template disappears from the queue, the graph and every dashboard, and the only way back is to set is_template to false explicitly on that subtree. If work vanishes after a reorganization, this is the first thing to check.
Nothing inside a template competes for attention
Template nodes are filtered out of almost every read path in the product. The action queue excludes them, the graph view excludes them at every depth, a dashboard's descendant walk stops at the first template node, the personal work list excludes them, and the general node list excludes them unless you pass include_templates. A half-finished template sitting in your workspace is not noise in anybody's day.
The write paths agree. Recurrence is refused on a template with the message “Repeating work must be a task, outside a template”, and the claim and lease operations refuse a template node outright, so an agent cannot pick up a step that only exists as a pattern. The flag is not decoration; it is the condition on a dozen queries.
The practical consequence is that you can draft a process in the open, over weeks, beside the real work it describes, without it counting as work. That is the right place to argue about structure, because nothing is on fire while you do it.
run_process takes a template and a parent, and nothing else
The input schema for run_process is exactly {template_id, parent_id?}. There are no parameters, no variables, no input form, no name override and no template version to pin. If the root is not a template the call fails with “Run requires a template root”. Everything the run will contain is already decided by the template before you call it.
POST /processes/nd_7Qk2VbR9xLm4pT0sWc1dEf/run
{ "parent_id": "nd_3Hb8ZpQ1mNv6yRt5uKw2Xa" }
201 Created
{
"id": "nd_Lc9WmT4rBq2ePz7hYd6kJn",
"template_id": "nd_7Qk2VbR9xLm4pT0sWc1dEf",
"node_count": 23
}The selection set is larger than the visible tree. It starts as the template's containment subtree, which is capped at a depth of 50, and then expands transitively across includes edges: every node the template includes is pulled in, and anything those nodes include after that. A shared sub-procedure referenced by an includes edge from three templates is copied into each run rather than shared between them.
That expansion is why the limit exists. A run is capped at 500 nodes, and exceeding it returns 422 invalid with “Template exceeds the 500-node run cap” and the count it reached. If you hit that ceiling, the fix is structural: split the process into a smaller root that includes fewer procedures, and run the parts separately. There is no flag that raises the cap.
What is copied, and what is deliberately left behind
Per node, the copy carries type, title, body_md, labels and metadata, and is given a fresh rank, a fresh workspace num and, for action types, a fresh queue_rank at the end of the queue. is_template is forced false on every copy, so a run is never accidentally a template. Nothing about the copy's identity is shared with the original.
State is normalized rather than copied. A null state stays null, so a page inside a template stays a page, and every other state becomes open. A template you left with three steps closed from the day you wrote it produces a run in which all of them are open. This is the detail that makes it safe to build a template out of a run that already happened.
The assignee is kept only if that principal is still assignable, and otherwise falls back to the workspace default assignee, then the caller, then the first owner. No step is created ownerless. Comments are not copied at all: the argument you had inside the template stays in the template, and the run starts with a clean discussion thread.
Body text is rewritten during the copy. Every old node id appearing in a body is replaced with the id of its new copy, so a wikilink from step two to step five inside a template resolves to the run's own step five rather than to the template's. Internal cross-references survive; that is the mechanism that makes them survive.
A link out of the template points at the shared node
Four edge types are carried into a run: blocks, reference, embed and includes. Each one is remapped where it can be — but only where both ends are inside the selection set. When the destination is outside the run, the new edge keeps the original destination id.
That is a real design consequence, not an implementation detail, and it cuts both ways. A reference from a template step to your standing security policy points every run at the one real policy node, which is what you want: one document, many runs, no drift. A blocks edge to a node outside the template blocks each run's step against that same shared node, which is what you want when the blocker is a genuine external gate and a problem when you meant “the previous step in this run”.
Other edge types are not copied at all. implements, verifies, supersedes, fulfills, mitigates and duplicates exist in the link registry, but a run does not carry them. If your template's steps verify a requirement, the run's steps do not, and you will be adding those edges by hand on every run. Design the template around the four types that survive.
A run is an edge, not a row
There is no runs table and no run object. A run is the copied tree, identified by its new root id, plus exactly one edge of type instance_of whose source is the new root and whose destination is the template root. That single edge is the entire relationship between a template and everything produced from it.
GET /processes/:template_id/runs is that edge query and nothing more. It joins edges where the type is instance_of and the destination is the template, and returns {id, title, state, created_at} newest first. The state you see in that list is the state of the run's root node, because that is the only state a run has.
A run has no status of its own. Its state is the state of a real node, and its history is the history of real nodes.
The operation writes an audit entry process.run recording {template_id, node_count} in the same transaction as the copy, so the count in the response and the count in the audit trail cannot disagree. If a run was created, the proof of it committed with it.
There is no step engine, so design as if there is none
Ordering inside a run is tree order plus rank. There is no step index, no sequence number and no field that opens step three when step two closes. Numbering your steps in their titles is not cosmetic here; it is the only sequence a reader gets beyond the order the nodes sit in.
Dependency is copied blocks edges, and those edges are enforced at close time only if the node's type declares a gated_close rule naming an edge type. Neither default type — doc and task — declares one, so out of the box a blocks edge inside a run is information for whoever reads it, not a stop. To make it a stop, create a type with rules: [{rule: "gated_close", via: "blocks"}], and closing a gated node with open blockers then returns 422 close_gated with the blocking nodes listed.
Nothing auto-advances, nothing branches, nothing loops, and a run sets no due dates or service levels of its own. Changing a template does not re-sync runs already created from it: the copy was taken once, and the improvement you made this morning reaches the next run only. Where teams get into trouble is expecting an engine that was never shipped and leaving out the structure that would have done the same job.
Spend the work on the structure, once
Everything above points the same way. The copy is faithful, cheap and unopinionated, which means the template is the only place where quality can enter. A vague step stays vague in fifty runs. A missing blocks edge is missing fifty times. A step nobody can start because it never says what “done” means costs an hour of asking each time somebody runs the process, and the copy does not care.
This is the argument for improving one real run before marking anything as a template. Run the process once with the team, watch where it stalls, and fold what you learned back into the structure: split the step that hides three decisions, delete the step nobody could explain, add the reference edge to the document people kept hunting for.
- Call
listProcessesand read the roots. Anything you do not recognize is a subtree somebody moved under a template by accident. - Run the template once into a scratch parent and read
node_countin the response. If it is far above what you expected, anincludesedge is pulling in a procedure you forgot about. - Open the run and check which edges arrived. Anything that was
implementsorverifiesin the template is not there, and either belongs asblocksorreferenceor belongs in the body. - Close a step that has an open blocker. If it closes, the node's type has no
gated_closerule, and your dependency is documentation rather than enforcement. - Count the steps with no stated outcome. Each one is a question somebody will ask on every future run.
None of these checks needs a feature TopDo does not have. They need someone to read the template as the thing that will be executed, because that is exactly what it is. The system will repeat your structure precisely; the work worth doing is making the structure worth repeating.