Welcome

Stigmery is a browser tool for agent-based modeling. You build a model out of many small agents that follow simple rules. Then you watch what pattern emerges.

It is inspired by NetLogo but built around two ideas:

  • A declarative model that you can read, edit by hand, save, and share.
  • The AI, which understands the same model and can build or change it in plain language.

You don't have to choose between them. The AI changes the model with the same actions you use in the editor. Watching the AI work is a good way to learn Stigmery.

This guide covers what you need to build or edit a model on the Code tab. New to agent-based modeling? Start with the Concepts chapter.

Core concepts

A Stigmery model has four moving parts.

World

A grid of squares called patches. You choose its width and height, and how big each patch is drawn. Its edges either wrap around (a torus) or act as hard walls.

Patches

The squares underneath everything. They can hold properties, for example grass (a number) or burning (true or false). They can also run their own rules each tick. Most simple models barely use patches. Models of landscapes and spreading lean on them heavily.

Agents

The agents move around on top of the patches. Every agent belongs to an agent type (e.g. sheep, wolf, boid) and carries:

  • a position (x, y) on the world,
  • a heading (degrees, 0 = east),
  • a unique id,
  • the properties you give its type, such as energy or age.

Appearance

Every agent type has an appearance:

shape:  'circle' | 'triangle' | 'square' | 'arrow'
color:  a CSS color or an expression that returns one
size:   a number or an expression

color and size are worked out for each agent, every frame. So an agent's look can follow its state. Wolves and Sheep puts colours in inner quotes, so the DSL reads them as text and not as names:

color: "'red'"
color: "energy > 5 ? 'green' : 'pink'"
size:  "1 + energy / 50"

Patches have an appearance.color expression too. The Wolves and Sheep model paints empty patches brown and grassy patches green with one line:

color: "grass > 0 ? 'green' : 'brown'"

Rules

The verbs of the model. A rule has a name, a when condition, and a do action. Rules live in one of four scopes:

  • setup_rules run once when you click Setup. Create your first agents here.
  • go_rules run once per tick, not for any one agent. Use them for model-wide bookkeeping.
  • agent_type.rules run for every agent of that type, every tick. Most of a model's rules live here.
  • patches.rules run for every patch, every tick.

Each tick runs the go_rules first. Then every patch runs the patches.rules, and then every agent runs its type's rules. Within a scope, rules run in the order you wrote them.

Building your first model

The fastest way to learn is to make a small model yourself. Click + New model in the library. You get an empty world.

1. Add an agent type

Open the Code tab. Under Agent types click + Agent type. Rename it to creature in the inspector.

2. Give it a property

Under creature, click + Property. Call it energy and set default to 50. This expression runs once for each agent at setup. Any DSL expression works there.

3. Add a setup rule

Under Setup rules click + Rule. The new rule's body is empty. Put this in it:

create_agents('creature', 30)

Now click Setup. Thirty agents appear, scattered randomly.

4. Add a rule for each tick

Under creature → Rules click + Rule. Body:

turn(random(-20, 20))
move_forward(1)
energy = energy - 1

This is a classic random walk that uses up energy. Click Setup, then Go. Your creatures wander around.

5. Add a die-off rule

Under creature → Rules, add another rule:

when:  energy <= 0
do:    die()

That's the whole loop: agents wander, lose energy and in the end die. Next you could plot the population over time, add food patches, or let creatures reproduce. Take it where you like.

Rules

A rule has three fields:

  • name: shown in the navigator and the change log.
  • when: a DSL expression that is true or false. Leave it blank and the rule always runs.
  • do: what happens when when is true. A DSL expression or a CodeBlock (see that chapter).

A simple rule

name:  eat_grass
when:  patch().grass > 0
do:    energy = energy + 4; patch().grass = 0

Toggling a rule

Every rule has an enabled switch. A disabled rule stays in the navigator but does not run. Use it to compare a model with and without that rule.

Scopes recap

  • setup_rules: run once at Setup. No self.
  • go_rules: run once per tick for the whole model. No self.
  • agent_type.rules: run for each agent. self is the current agent.
  • patches.rules: run for each patch. self is the current patch.

The DSL

The when and do fields take a tiny expression language. It is small on purpose, so a rule reads in one line. Need more? Use a CodeBlock (next chapter).

The full reference is one click away in the rule editor. The ? DSL button lists every function and name by category. This chapter explains the shape of the language.

Operators

+ - * / %            arithmetic
== != < <= > >=      comparison (=== and !== work too, same meaning)
&& || !              boolean
? :                  ternary
=  +=  -=  *=  /=    assignment (only inside do)
;                    statement separator

Numeric helpers

random(min, max)        float in [min, max), seeded RNG
random_int(min, max)    integer in [min, max] inclusive
random_normal(mean, sd) Gaussian sample
chance(p)               true with probability p
sqrt(x)  abs(x)  floor(x)  round(x)
min(...)  max(...)      smallest / largest argument
Math.PI  Math.atan2(y, x)  ...

Use the seeded helpers (random, chance) where you can. With them, the same seed gives the same run every time. Math.random() works, but then runs no longer repeat.

Movement (agent rules)

move_forward(d)         walk d patches along the current heading
turn(deg)               rotate heading by deg degrees (negative = left)
turn_random()           pick a uniform random heading
face(target)            turn to face an agent or patch
set_position(x, y)      teleport; wrapping rules apply
distance_to(target)     distance to an agent or patch

Patches and agents nearby

patch()                 the patch under the current agent
patch_ahead(d)          the patch d steps ahead in the current heading
patches_in(r)           patches within radius r
neighbors()             8 surrounding patches (patch rules) /
                        nearby agents (agent rules)
neighbors4()            4 cardinal neighbour patches (Von Neumann)
agents_here()           every agent currently on this patch (patch rules)
all_patches()           every patch in the world (observer)
all_agents()            every alive agent (observer)
agents_of_type('foo')   every alive agent of that type
nearest('foo')          the closest agent of type 'foo', or null

Agent lifecycle

create_agents('foo', n)        spawn n agents at random positions (observer)
create_agents_at('foo', n,x,y) spawn n agents at exact (x, y) (observer)
spawn('foo')                   one new agent at the current spot (agent rule)
spawn_at('foo', x, y)          one new agent at (x, y) (agent rule)
sprout('foo')                  one new agent at this patch (patch rule)
hatch(count?, init?)           clone the current agent; run init(child) on each
die()                          remove self at end of tick

You don't have to write create_agents in a setup rule. Every agent type has an initial_count. If no setup rule creates agents, Setup creates that many at random positions.

Diffusion (patch properties)

Two helpers spread a number property of the patches to their neighbours each tick. Use them for chemicals, pheromone trails, heat and the like. They work for the whole model, so they go in a go_rule:

diffuse('pheromone', 0.5)   // 8-neighbour Moore stencil
diffuse4('heat', 0.2)       // 4-neighbour Von Neumann stencil

The second argument is the share of each patch's value that spreads out each tick.

Links (networks)

Agents can be joined by links that have no direction. Together they form a network on top of the population. Create and inspect them with:

link(a, b)              // observer or agent rules
unlink(a, b)
linked(a, b)            // observer: bool
my_links()              // agent: links touching this agent
link_neighbors()        // agent: every agent I'm linked to
all_links()             // observer: every link in the world

Aggregations

count, sum, mean, min and max work on arrays. count takes any array: agents, patches, anything. The others need numbers, so use .map to pick out a property first:

count(agents_of_type('sheep'))                            // 47
count(agents_of_type('sheep').filter(a => a.energy > 5))  // 33
mean(agents_of_type('sheep').map(a => a.energy))          // 12.4
sum(all_patches().map(p => p.grass))                      // 1280

A common slip is mean(agents_of_type('sheep').energy). It reads .energy on the array, which has none. Always .map first.

Lambdas

Anonymous functions are allowed wherever an array method (.filter, .map, .sort, .reduce) or the ask helper needs a callback. Three shapes work:

x => x.energy > 0                       single arg
(a, b) => a.energy - b.energy           multi arg, e.g. sort comparator
() => spawn('child')                    zero arg, for ask(arr, () => ...)

ask(collection, fn) calls fn for each item. An error in one item does not stop the others.

me and globals

In agent or patch rules, me is the current agent or patch. A bare name works when nothing else has it. energy = energy - 1 is the same as me.properties.energy = me.properties.energy - 1. Use me. when a global or a function has the same name.

Sliders, switches and choosers on the Interface tab are bare names in rules. A slider named sheep_repro is just sheep_repro in an expression.

When a rule fails

A when or do can fail: a typo, an unknown name, a bad value. Then the rule is skipped for that agent or patch on that tick. One bad rule never stops the model. The message is kept in RuntimeState.rule_errors. A red ! error badge shows it next to the rule in the Code tab. The badge goes as soon as the rule runs cleanly.

CodeBlock escape

The DSL covers most simple rules. Need loops, several steps of changes or helper code? Set the do field to a CodeBlock.

In the editor, tick the CodeBlock (TS escape) box above the code editor. The body takes JavaScript-like code and sees what a DSL expression sees. Bare names are the current agent's or patch's properties and the globals. All DSL functions work, and me is the current agent or patch.

Example: flocking-style alignment

const others = neighbors(3);            // agents within 3 patches
if (others.length === 0) return;

// average heading of nearby boids
let sx = 0, sy = 0;
for (const b of others) {
  sx += Math.cos(b.heading * Math.PI / 180);
  sy += Math.sin(b.heading * Math.PI / 180);
}
const target = Math.atan2(sy, sx) * 180 / Math.PI;

// rotate 10% of the way toward the group's average heading
turn(0.1 * (target - heading));
move_forward(1);

What's allowed

  • Assignments, conditionals, loops (for, while), local let/const.
  • The full DSL function set (move_forward, neighbors, chance, ...).
  • Standard JS globals: Math, JSON, Array, Object, Number, String, Boolean, parseInt, parseFloat, isNaN, isFinite. These are also available in regular one-line expressions.

What's banned

  • await / async: a rule must finish within its tick.
  • eval, new Function(...), import(...): no loading of code.

A rule that uses them fails when it is read. It then shows a red ! error badge next to its name in the Code tab.

Properties & globals

There are two kinds of named state in a model.

Properties

Each agent type, and the patches, can have properties. Every agent of that type, or every patch, gets its own value.

sheep: { properties: [
  { name: 'energy', type: 'number', default: 'random(10, 50)' }
]}

The default is a DSL expression that runs once for each agent at setup. In rules you read and write the property as a bare name (energy = energy + 1). Write me.properties.energy when another name is the same.

Globals

Globals are values of the whole model, each with a control on the Interface tab:

  • Slider: a number with a min, max and step size.
  • Switch: on or off.
  • Chooser: one value from a list.
  • Input: any text or number.

Rules read a global by its bare name. A slider grass_regrow_rate is grass_regrow_rate in an expression. You can change globals while the model runs.

Naming

A property or global name uses letters, digits and underscores, and does not start with a digit. It must be unique in its scope. The form refuses reserved words and names already taken.

Plots & monitors

You need to see what is happening in a model. Metrics, plots and monitors show it.

Metrics

A metric is a named expression for the whole model, worked out each tick. Metrics live in model.metrics. Plots and monitors show them.

sheep_count:   count(agents_of_type('sheep'))
grass_total:   sum(all_patches().map(p => p.grass))

Plots

A plot draws one or more metrics over time. Each line is a pen, and you choose its colour. Plots are in the Observation panel on the Interface tab.

Monitors

A monitor shows the current value of one metric in big numbers. Use one for a single number, such as the population or the total energy.

Exporting

Open the library and click the ⬇ icon on the model's row. You get the whole model as a JSON file. Exporting a run's metrics as CSV is planned.

Working with the AI

The AI is not a general chatbot. It changes your model with the same tools you use. It can add an agent type or a rule, set a global, run Setup and tick. Its changes go into the same change log. On a wide screen it sits beside the workspace. On a phone, tap AI in the top bar. Watching it work is a good way to learn Stigmery.

Good prompts

  • Be specific about behaviour, not syntax. "Make the sheep wander randomly and lose energy each tick" works better than "write me a random-walk function".
  • Ask for one thing at a time. You get a faster result that is easier to check.
  • Iterate. "Now add wolves that chase the nearest sheep." "Now plot the populations." You can build a model piece by piece.

Things to ask the AI to do

  • "Build me a flocking model with 30 boids."
  • "Set sheep_repro to 0.10 and run 50 ticks. What's the sheep_count?"
  • "Switch to Game of Life."
  • "Why does the wolf population crash so fast?"

Things to do yourself instead

  • Fine-tune a slider's default. It is quicker by hand.
  • Choose which plot to show. The AI can't see your plot.
  • Rename things. Go straight to the navigator.

Library & sharing

You open, save, move and share models in the library. Open it from the top bar.

My models vs Examples

  • Examples are built-in models you can open but not change. Double-click to open one. Or point at its row and click ⬇ to download its JSON.
  • My models are the models you saved. Saving into a path makes it a folder. The folder goes when its last model moves out. You can also make an empty folder with + New folder.

Actions

  • Save: opens the library with a save banner. Pick a folder in the tree, set the name and who can see it, and click Save here.
  • Auto-save: a saved model saves itself 2 seconds after each edit. The Save button in the top bar shows Saving… for a moment.
  • Move: drag a model's row onto a folder. Drop it in the tree, on the folder path or on a subfolder tile.
  • Rename or describe: click the title or description in the model's details.
  • Delete: point at a row, click the 🗑 icon, confirm.
  • Download: point at a row, click ⬇.

Sharing

Select your model in the library. In its details, set Sharing to Public. A share link appears. Anyone with that link can view the model, but not edit it. Set it back to Private and the link stops working at once.

Simulation controls

The bar above the workspace controls the run.

  • Setup: starts the world again. It runs every setup_rules, sets the tick counter to 0 and seeds the random numbers from Seed.
  • ← Step: goes back one tick. Works from tick 1 on.
  • Step →: runs one tick.
  • Go / Pause: runs ticks until you pause.
  • Tick / Agents / Seed, on a wide screen: live counters. Click the seed to edit it, or click ↻ for a random one. Either runs Setup again.
  • Undo, on a wide screen: takes back the last change to the model's structure. That is adding, changing or deleting an agent type, rule, property and so on. Ticks and slider changes stay.

Time travel

Every Setup starts a fresh history. Running forward saves checkpoints. ← Step replays from the nearest checkpoint up to tick - 1. A change to the structure, such as a rule or an agent type, sets the model up again. That starts a fresh history too.

Tips & gotchas

Iterate small

Add one rule, click Step a few times, and see what changes. With the change log and Undo, you can experiment without fear.

Use Setup early and often

Setup always gives the same starting state. If something looks odd, Setup brings the model back to it.

Use the seed when reproducing bugs

If a run looks wrong, note its seed before you report it or ask the AI. On a wide screen the seed is in the bar above the workspace. On every screen it is under World settings (the cog on the World panel). The same seed gives the same run.

Patches before agents, sometimes

Does the landscape change, as with regrowing grass, fire or pollution? Add the patch property first. Then add the agent rules that read it. That way the setup is easier to follow.

When something doesn't behave

  • Did the world start over when you edited a rule? A change to a rule, property, agent type or the world's settings sets the model up again. The next Go runs the model as it now stands. Changing a slider or another global leaves the run as it is.
  • Is the rule enabled? The toggle is right there in the rule editor.
  • Is the agent type initial_count non-zero (or do you create them yourself in a setup rule)?

Feedback & contact

Found a bug, have an idea, or just want to share what you built? Email marvin.kleijweg@aivoorimpact.nl.

Citing Stigmery

If you use Stigmery in academic or published work, please cite it as:

> Kleijweg, M. (2026). Stigmery: Browser-based agent-based modeling with an AI copilot [Computer software]. https://stigmery.com