Skip to main content

petrinaut

Using the Petrinaut tools and writing net code. Use before the first net change, before writing lambda, kernel, dynamics, visualizer, metric or scenario code, before drafting or creating an experiment, and when the USER asks how something in the Petrinaut UI works.

Source facts

Repository
hashintel/hash
Last source activity
October 15, 2026 at 08:01
Detected SKILL.md language
English
Stars
1,675
Forks
124

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
petrinaut
description
Using the Petrinaut tools and writing net code. Use before the first net change, before writing lambda, kernel, dynamics, visualizer, metric or scenario code, before drafting or creating an experiment, and when the USER asks how something in the Petrinaut UI works.
# Petrinaut ## Routing When the USER asks how a Petrinaut UI workflow works, or you need to confirm a UI detail before instructing them, read `references/docs.md` for the user-guide pages `readPetrinautDoc` can return. When you need a worked example of a complete net definition (coloured tokens, stochastic and predicate transitions, kernels with distributions, continuous dynamics, parameters, visualizer code, metrics and scenarios), read `references/examples.md`. Before drafting or creating an experiment, read `references/experiments.md` for the request's fields and limits and what the draft and run tools do. ## Tools Make every net change through the mounted tools; never emit free-form net JSON. They use Petrinaut's raw mutation interfaces, so include stable IDs, full entity objects where required, and canvas positions for places and transitions. The mounted schemas, not this prose, govern exact payload fields. You can check current TypeScript compilation diagnostics at any point using the getNetCompilationErrors tool, and rename the net using the setNetTitle tool. ### Reading the net Three tools read the current net, each returning `{ title, definition, extensions }`: the user-visible net title, the SDCPN, and the active extension capabilities for this document. - `readNetOutline` leaves out code and expressions (including parameter overrides, component parameter values and scenario initial states beyond the shape of an ad-hoc one), canvas positions and visual settings. - `readNetStructure` adds that code and those expressions. - `getLatestNetDefinition` returns the complete definition, including positions and visualizer code. A change is refused unless a current read of the net is in context, so read it before the first change, and again when the USER may have edited the net since. After a step of changes, use the last result's `netAfterChanges` instead of rereading. Read again only for code or fields `netAfterChanges` omits, before a live explanation, and at delivery. Consult `extensions` before authoring extension-specific content, and `title` when deciding whether the net needs a more descriptive name. ### Making changes Send one bounded connected fragment in one step, as parallel mutation calls in dependency order: the types, parameters and differential equations it needs; then places and transitions; then arcs. The host runs a step's calls in that order. You choose every new ID, so the calls that build one fragment do not depend on each other's results. References to existing elements, endpoints and experiment inputs use identifiers observed in a read, never IDs guessed from names. The host owns immutable binding, protocol correlation, document-base checks, persistence and record attachment. Never copy document hashes, document revisions, observation call IDs or Ledger record IDs into tool inputs. On a failure, a no-op or an unknown outcome, inspect the result and the current net, then submit only the correction. Never repeat a change that succeeded or may have. ### Keep incidental choices small Required `x` and `y` are provisional presentation values: place new nodes on a rough grid, then use layout. For a new type, `iconSlug: "circle"` and a simple CSS `displayColor` are sufficient. Optional metadata and port fields are unnecessary for a simple root-net fragment; use them only when the task needs their capability. For separately wired transitions, start with empty `inputArcs` and `outputArcs`, then use `addArc` operations. Uncoloured places use `colorId: null`; disabled dynamics use `dynamicsEnabled: false` and `differentialEquationId: null`. Keep code strings empty only where their schema permits the built-in behaviour; coloured outputs or a meaningful guard or rate require the corresponding code. Arc weights are token multiplicities, never branch probabilities. ### Acceptance An accepted call establishes conformance to its input schema, and nothing more. After each step check that: - every intended call was accepted, or its rejection remains explicitly unresolved; - `netAfterChanges` contains each accepted element under the ID you supplied or the tool returned; - every referenced endpoint exists; - arc weights are positive; - no later step depends on a rejected or absent change. Describe this result as tool-schema accepted, not valid, runnable or simulated. ## Extensions - Check the active `extensions` from your latest read before using optional SDCPN features. If an extension is disabled, do not create or rely on its data. - Coloured-token types require `extensions.colors`. - Parameters require `extensions.parameters`. - Stochastic transition lambdas, for rate-based firing, require `extensions.stochasticity`. - Use predicate transition lambdas for boolean firing conditions when `extensions.stochasticity` is true, or when `extensions.colors` is true and the transition has at least one standard or read input arc from a coloured place. - Leave transition lambda code empty when neither stochasticity nor coloured standard/read inputs are available; the runtime treats the transition as always enabled once its arc weights are satisfied. - Use transition kernels to transform or generate coloured output tokens. Use stochastic distributions in kernel outputs only when `extensions.stochasticity` is true. Leave kernel code empty when the transition has no coloured output places. - Differential equations require both `extensions.colors` and `extensions.dynamics`, and apply only to places whose coloured tokens have continuous dynamics. ## Writing net code Lambda, kernel, and dynamics code is a plain function body ending in `return` — no module, no `export default`, no wrapper — with the input object and `parameters` available ambiently. (The legacy `export default Lambda((input, parameters) => …)` / `TransitionKernel(…)` / `Dynamics(…)` module form is still accepted, but write new code as a bare body.) Every code surface except visualizers accepts only a small TypeScript subset: `const` bindings, destructuring, guard-style `if … return`, ternaries, arithmetic, comparison and logic, `Math.*`, `.length`, `.map(...)`, record and array literals, and in metrics also `.reduce(...)` and `.concat(...)`. Loops, `let` and `var`, mutation such as `.push(...)`, spread and other helper calls are rejected. To build `n` tokens, write `Array.from({ length: n }, (_, index) => ({ … }))` instead of a loop. ### Code surfaces The exact shapes the runtime expects: - Transition lambda (`transition.lambdaCode`): function body returning the firing condition; `input` and `parameters` are ambient. Available when stochasticity is enabled OR when colours are enabled and the transition has at least one standard or read input arc from a coloured place. `input.PlaceName` is a tuple sized to the input arc weight for coloured standard and read input arcs; token attributes are typed by the colour element: real/integer → number, boolean → boolean, uuid → bigint, string → string (plain JS strings everywhere, compared by value). Read arcs expose tokens in `input` but do not consume them when the transition fires. Inhibitor arcs and uncoloured input places are NOT in `input`. Predicate → return a boolean; stochastic → return a non-negative rate in firings per simulation second (0 disables, Infinity always fires). Must be deterministic. If unavailable or empty, the runtime uses true for predicate-style transitions and Infinity for stochastic-style transitions. Example: `return input.Queue[0].priority > parameters.threshold;`. - Transition kernel (`transition.transitionKernelCode`): function body returning `{ OutputPlaceName: [token, …] }` sized to the output arc weight; `input` and `parameters` are ambient. Available only for transitions with coloured output places. Include only coloured output places; uncoloured output places are auto-populated. Output values must match element types: real/integer use numbers, boolean uses booleans, string uses plain strings (REQUIRED in the token type; a missing/undefined value becomes the empty string `""`, and non-string values are stringified via `String(value)`). uuid attributes are OPTIONAL in output tokens: omit them to auto-generate a fresh UUID deterministically from the seeded simulation RNG, use `Uuid.generate()` for an explicit fresh UUID, `Uuid.from(value)` to derive one from a number or arbitrary string, use a UUID string directly, or forward an input token's uuid bigint unchanged. Plain non-UUID values must be wrapped in `Uuid.from(...)`; supplying them directly is a type error. When stochasticity is enabled, real attributes may use `Distribution.Gaussian(mean, sd)` / `Distribution.Uniform(min, max)` / `Distribution.Lognormal(mu, sigma)` (never integer/boolean/uuid/string attributes), and chained `.map(fn)` on the same distribution shares one draw. When stochasticity is disabled, kernel outputs must use plain values only. Leave empty when no coloured outputs exist. Example: `return { Processed: [{ id: input.Queue[0].id, done: true }] };`. - Differential equation (`differentialEquation.code`): function body returning the derivatives array; `tokens` and `parameters` are ambient. `tokens` is THIS place's tokens only. Return an array of the same length whose entries provide derivatives for real-valued elements only (i.e. dx/dt, not the new value); integer, boolean, uuid, and string elements are discrete and remain unchanged by dynamics (they can be read from input tokens but never written). The equation's `colorId` MUST match every referencing place's `colorId`. Example: `return tokens.map(({ level }) => ({ level: -level * parameters.decay_rate }));`. - Place visualizer (`place.visualizerCode`): `export default Visualization(({ tokens, parameters }) => <JSX/>)`. Classic React runtime — do NOT import React, do NOT use `<>…</>` fragments, do NOT use hooks. Convention: return a sized `<svg viewBox="0 0 W H">…</svg>`. - Metric (`metric.code`): a plain function body — NOT a module, no `export default`, no wrapper. `state` is in scope, and net `parameters` are available ambiently (read them as `parameters.<variableName>`). Must `return` a finite number. Example: `return state.places.Infected.count / (state.places.Susceptible.count + state.places.Infected.count + state.places.Recovered.count);`. `scenario` parameters are NOT available inside metrics (only net parameters are). - Scenario per_place initial state: `content` keys are place IDs; uncoloured values are expressions with `parameters` and `scenario` in scope; coloured values are row arrays in colour element order using numbers and booleans; string columns take literal text; uuid columns accept UUID strings (any other text converts deterministically to a UUID via UUIDv5). - Scenario code-mode initial state: function body returning `{ PlaceName: tokens }` keyed by NAME (asymmetric with per_place IDs); unknown names are silently dropped. - Scenario parameter wiring: a `per_place` initial-state expression reads a scenario parameter as `scenario.<identifier>`, and `parameterOverrides` maps an existing net parameter ID to such an expression. A value that transition code reads through `parameters.<variableName>` therefore needs both the net parameter and the override. Metrics cannot read scenario parameters. - Parameter access in any code surface: use `parameters.<variableName>` where `<variableName>` is the parameter's lower_snake_case `variableName` value (e.g. `parameters.crash_threshold`, never `parameters.crashThreshold`). ### Durations A stochastic lambda's rate makes the time to firing exponentially distributed, with mean `1 / rate`. Use a rate only for what is random in time, such as arrivals or breakdowns. For a fixed or otherwise distributed duration, count it down on the token instead: 1. give the in-progress place's colour a real `remaining_time` element; 2. set it in the starting transition's kernel: a constant or `parameters.<variableName>` for a fixed duration, a `Distribution` for a spread; 3. give the in-progress place a differential equation returning `-1` for it: `return tokens.map(() => ({ remaining_time: -1 }));`; 4. guard the completing transition with a predicate: `return input.InProgress[0].remaining_time <= 0;`. This needs `extensions.colors` and `extensions.dynamics`, and `extensions.stochasticity` for a `Distribution`. Durations are in simulation time units. The `useful-patterns` page that `readPetrinautDoc` returns has the full example. ### After a code change Validate every code-writing change. After the step that writes code — lambda, transition kernel, dynamics, visualizer, metric, or scenario code-mode initial state — call getNetCompilationErrors once for the whole step, and send every repair it calls for in one further step before relying on the new code. Mutations validate only the schema, not the runtime contract. Saved scenario and metric code is compiled separately, when an experiment is created, so a clean diagnostic does not prove it. Place names are part of the code surface, so they must be identifiers matching `^[A-Z][a-zA-Z]*\d*$`: letters only, starting uppercase, digits only at the end (`TankLevel`, `Bay2`; never `Tank_Level`, `Level5m` or `Last reading`). Lambdas/kernels read `input.PlaceName`, metrics read `state.places.PlaceName.count`, and scenario code-mode initial state keys are place names. Renaming a place via `updatePlace` requires updating every dependent lambda, kernel, dynamics, metric, visualizer, and scenario in the same batch — otherwise you will silently break references. ## Finishing a change - Keep executable code self-contained and readable. - Title the net. After building or substantially extending a model, check the title from your latest read. If it is `Untitled` or an obvious placeholder, call `setNetTitle` with a concise, descriptive title (sentence case, ideally under ~60 characters). Don't overwrite a USER-chosen title without being asked. - Place visualizers are compact, single-glance SVGs sized for a place node, following the place visualizer rules under Code surfaces. Auto-layout policy. Once you've finished adding or restructuring places and transitions, call `applyAutoLayout` so the canvas isn't littered with overlapping nodes at the origin. Pass `askUserFirst: false` ONLY when the net was empty at the start of the conversation and you built it from scratch. If USER-arranged content existed beforehand — even if you only added a few nodes to it — pass `askUserFirst: true` and the USER will be shown a Yes/No prompt. If they decline, leave the layout alone and continue without retrying unless they ask.
View on GitHub