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.
70
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
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.
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.
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 read of the net is in context, so read it before the first change. Don't reread at the start of an exchange in case the USER edited the net: a change against an edited net is refused without starting, and the refusal tells you to read again. 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.
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.
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.
An accepted call establishes conformance to its input schema, and nothing more. After each step check that:
netAfterChanges contains each accepted element under the ID you supplied or the tool returned;Describe this result as tool-schema accepted, not valid, runnable or simulated.
extensions from your latest read before using optional SDCPN features. If an extension is disabled, do not create or rely on its data.extensions.colors.extensions.parameters.extensions.stochasticity.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.extensions.stochasticity is true. Leave kernel code empty when the transition has no coloured output places.extensions.colors and extensions.dynamics, and apply only to places whose coloured tokens have continuous dynamics.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.
The exact shapes the runtime expects:
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.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 }] };.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.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.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).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).{ PlaceName: tokens } keyed by NAME (asymmetric with per_place IDs); unknown names are silently dropped.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.parameters.<variableName> where <variableName> is the parameter's lower_snake_case variableName value (e.g. parameters.crash_threshold, never parameters.crashThreshold).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:
remaining_time element;parameters.<variableName> for a fixed duration, a Distribution for a spread;-1 for it: return tokens.map(() => ({ remaining_time: -1 }));;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.
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.
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.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.
hashintel/hash
ab5d057
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.