Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Anvil Book

Learn to use Anvil from nothing: install it, write test steps, put them in sequences, run them, and read what Anvil says back.

Alpha. This edition is written and checked against Anvil 0.5.0 on Linux x86-64; installing on Windows is covered and checked too. Anvil is young, and some of what you will do here — cloning the repository to get the step SDK, above all — will get shorter. Where 0.5.0 has a rough edge or a known defect, the book says so where you meet it, rather than pretending it is not there.

Contents

  1. Introduction — what Anvil is, the words it uses, and what you need.
  2. Installing — the engine, the step SDK, your working folder.
  3. Your first step — a C# method the engine can call.
  4. Your first sequence — the file that says what to run, and how to run it.
  5. Measuring and judging — limits, pass, fail and error.
  6. Setup, main, cleanup and retries
  7. Inputs, variables and flow
  8. Instruments that stay open — references to objects that live in the executor.
  9. Subsequences
  10. Reports and running unattended
  11. The Sequence Editor — not written yet. In 0.5.0 the packaged editor opens, but it cannot run a sequence (#80).
  12. Steps in Python and Rust
  13. When something goes wrong

Read chapters 1 to 4 in order. After that each chapter stands mostly on its own, although each one adds a file or two to the same C# project.

Conventions

Everything happens in one folder, ~/anvil-book. Commands start with $ and are run from that folder, unless the text says otherwise. The lines after a command are what it printed — pasted from a real run, not retyped.

Anvil’s own diagnostic messages are still partly in Spanish in 0.5.0 (#59). The book shows them as they are printed and explains what they say.

How this book stays true

Every command in these chapters has been run against the release named above, and none of the output was written by hand. The files you are asked to write live in listings/, the terminal sessions in listings/sessions/, and check.sh runs every session again and checks that each file still appears word for word in a chapter:

$ docs/book/check.sh path/to/anvil-v0.5.0-x86_64-linux-musl

What could not be run is marked not verified, with those words.

1. Introduction

Anvil is a test sequencer: it runs a list of test steps against a unit on a bench, judges what they measure and writes down what happened. If you have used NI TestStand or OpenTAP, the job is the same one.

What makes Anvil different is where things live.

  • A sequence is a text file, not a program. It lists the steps, the limits each measurement must fall within, and what to do before and after. You can read it in a diff, and changing a limit does not mean recompiling anything.
  • A step is ordinary code in your language — C#, Python or Rust — served by an executor: a small process (or, for Rust, a module) that publishes the steps it knows how to run. Anvil calls each step by name.
  • The step measures; Anvil judges. A step returns a number. Whether that number is acceptable is written in the sequence, and Anvil decides. The same step can serve two products with different tolerances without being touched.

The words

WordWhat it means here
sequenceA .yseq (or .yaml) file: the steps to run and how to judge them.
stepOne action or measurement, called by a name like board/measure_rail.
executorThe program that runs steps. A sequence says where each one is.
moduleThe first half of a step’s name. In C# it comes from the class.
limitThe acceptance criterion for a measurement, written in the sequence.
verdictWhat a step or a sequence ended as: pass, fail, error or skipped.

The difference between fail and error matters more than anything else in this book, and chapter 5 is about it: fail says something about the unit, error says something about the bench. A unit that measures out of range fails. An instrument that does not answer is an error — Anvil could not judge the unit, so it does not pretend to.

What this book covers

Writing steps in C#, writing sequences, running them from the command line and reading the results. One chapter shows what changes when the step is written in Python or Rust instead.

It does not cover talking to real instruments — every step here pretends to measure — nor how Anvil is built inside. For that, see the documentation index.

What you need

  • Linux or Windows, on x86-64. Everything in the book was run on Linux. Chapter 2 also covers installing on Windows, and says what of the rest was run there.
  • The .NET 10 SDK (dotnet --version should print 10. something).
  • git, to get the step SDK.
  • For chapter 12 only: Python 3.10 or newer, and a Rust toolchain.

No instrument, no licence and no network connection to a bench.

2. Installing

You will end this chapter with one folder holding three things: the Anvil engine, a copy of the Anvil repository (for the C# step SDK), and an empty C# project where your steps will go.

$ mkdir ~/anvil-book
$ cd ~/anvil-book

The engine

Anvil is one statically linked binary; there is nothing to install system-wide. Download it from the release page, check it, and unpack it:

$ curl -sSLO https://github.com/anlaco/anvil/releases/download/v0.5.0/anvil-v0.5.0-x86_64-linux-musl.tar.gz
$ curl -sSLO https://github.com/anlaco/anvil/releases/download/v0.5.0/SHA256SUMS
$ sha256sum --check --ignore-missing SHA256SUMS
anvil-v0.5.0-x86_64-linux-musl.tar.gz: OK
$ tar xzf anvil-v0.5.0-x86_64-linux-musl.tar.gz

(sha256sum prints OK in your system’s language.)

The folder it creates holds anvil, the engine, and anvil-exec-wasm, which chapter 12 uses. Put it on your PATH — in every new terminal you open for this book:

$ export PATH="$PWD/anvil-v0.5.0-x86_64-linux-musl:$PATH"

and check:

$ anvil --version
anvil 0.5.0

anvil --version writes to the error stream, not to standard output, so anvil --version | something reads nothing (#75).

On Windows, follow the Windows section at the end of this chapter instead.

The step SDK

Your steps will use a small library, Anvil.Step. In 0.5.0 it is not yet published on NuGet, so you take it from the repository, at the tag that matches your engine:

$ git clone --depth 1 --branch v0.5.0 https://github.com/anlaco/anvil.git anvil

git will print a note about a “detached HEAD”: that is what checking out a tag looks like, and it is fine. You will not build Anvil from this copy — you only point your project at the SDK inside it. When the package reaches NuGet, this step goes away.

The project

$ dotnet --version
$ dotnet new console --output Bench

The first command must print a version starting with 10.. The second creates Bench/, the executor you will fill with steps from chapter 3 on.

Your folder now looks like this:

~/anvil-book/
├── anvil/                               the repository, for the SDK
├── anvil-v0.5.0-x86_64-linux-musl/      the engine
└── Bench/                               your steps

The Sequence Editor

The release also carries the Sequence Editor, a graphical editor for the same files, as anvil-editor-v0.5.0-x86_64-linux.AppImage, a .deb and a Windows installer. This book does not use it yet (see the contents).

If you try the AppImage and it stops at dlopen(): error loading libfuse.so.2, your system lacks FUSE 2, which recent Ubuntu releases no longer install. The AppImage runs without it once extracted:

$ chmod +x anvil-editor-v0.5.0-x86_64-linux.AppImage
$ ./anvil-editor-v0.5.0-x86_64-linux.AppImage --appimage-extract
$ ./squashfs-root/AppRun

On Windows

Everything above has a Windows equivalent, in Windows PowerShell — the one Windows already has; open it from the Start menu. This section was run on a GitHub Actions Windows machine (Windows Server 2025, build 26100, Windows PowerShell 5.1), with the downloads marked as coming from the Internet as a browser marks them. It has not been run on a Windows desktop: what Windows’ own security prompts do there is not verified.

Download

On the release page, under Assets, download two files. Your browser saves them in Downloads:

  • anvil-v0.5.0-x86_64-windows.zip — the engine;
  • SHA256SUMS — the checksums, to confirm the download is intact.

Check and unpack

PS> mkdir $HOME\anvil-book
PS> cd $HOME\anvil-book
PS> Move-Item "$HOME\Downloads\anvil-v0.5.0-x86_64-windows.zip", "$HOME\Downloads\SHA256SUMS" .
PS> (Get-FileHash .\anvil-v0.5.0-x86_64-windows.zip -Algorithm SHA256).Hash
2F2AA5AF9044B2BBBA42E66434361EBC220EF1849E73F927FFC50121307EC96A
PS> Select-String anvil-v0.5.0-x86_64-windows.zip .\SHA256SUMS
C:\Users\you\anvil-book\SHA256SUMS:5:2f2aa5af9044b2bbba42e66434361ebc220ef1849e73f927ffc50121307ec96a  anvil-v0.5.0-x86_64-windows.zip

The two hashes must be the same digits; PowerShell prints them in capitals and the file in lower case. Then unpack:

PS> Expand-Archive .\anvil-v0.5.0-x86_64-windows.zip -DestinationPath .

It creates anvil-v0.5.0-x86_64-windows\, with anvil.exe and anvil-exec-wasm.exe. They are built not to need the Visual C++ runtime; the test machine had it installed anyway, so a machine without it is not verified.

If you unpack with the Explorer instead (right click → Extract All), the files inside may keep the “downloaded from the Internet” mark. On the test machine anvil.exe ran from PowerShell with the mark on; if your Windows refuses to run it, remove the mark from the whole folder:

PS> Get-ChildItem -Recurse .\anvil-v0.5.0-x86_64-windows | Unblock-File

Neither anvil.exe nor the editor installer is digitally signed in 0.5.0, so Windows may warn before running them. That warning was not reproduced here.

Put it on the path

In every PowerShell window you open for this book:

PS> $env:Path = "$PWD\anvil-v0.5.0-x86_64-windows;$env:Path"
PS> anvil --version
anvil 0.5.0

The SDK and the project

The same commands as on Linux, and git and the .NET 10 SDK must be installed first:

PS> git clone --depth 1 --branch v0.5.0 https://github.com/anlaco/anvil.git anvil
PS> dotnet --version
PS> dotnet new console --output Bench
C:\Users\you\anvil-book\
├── anvil\                               the repository, for the SDK
├── anvil-v0.5.0-x86_64-windows\         the engine
└── Bench\                               your steps

The rest of the book on Windows

The chapters are written for a Linux shell. Chapters 3 and 4 were also run on the Windows machine, up to running sequences/first.yseq and reading its JSON report: the C# project, dotnet run and anvil behave the same and print the same output. Paths work with either / or \. Where a command in the book is shell syntax, use:

in the bookin Windows PowerShell
~/anvil-book$HOME\anvil-book
export PATH="$PWD/anvil-v0.5.0-x86_64-linux-musl:$PATH"$env:Path = "$PWD\anvil-v0.5.0-x86_64-windows;$env:Path"
2>/dev/null2>$null
echo $?$LASTEXITCODE
cat data.jsonGet-Content data.json
ss -ltn | grep 9201Get-NetTCPConnection -LocalPort 9201 -State Listen

A few sessions pipe into tail or grep only to shorten what they show; on Windows, run the command without them. Chapters 5 to 13 and the Python and Rust steps of chapter 12 have not been run on Windows.

The Sequence Editor on Windows

Download anvil-editor-v0.5.0-x86_64-windows-setup.exe from the same page and run it. On the test machine it installed for the current user, into %LOCALAPPDATA%\Programs\@anvileditor\, and added Anvil Sequence Editor to the Start menu. That was checked with a silent install (setup.exe /S); the installer’s own window, and whether it asks for administrator rights, were not. Remember that in 0.5.0 the packaged editor cannot run a sequence (#80).

3. Your first step

A step is a C# method with an attribute on it. In this chapter you write one that pretends to measure a supply rail, and you turn Bench/ into an executor that serves it.

Point the project at the SDK

Replace the contents of Bench/Bench.csproj with:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="../anvil/executors/csharp/src/Anvil.Step/Anvil.Step.csproj" />
    <ProjectReference Include="../anvil/executors/csharp/src/Anvil.Step.Generator/Anvil.Step.Generator.csproj"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
  </ItemGroup>

</Project>

The two references are the SDK and its generator: at compile time, the generator reads your methods and writes the list of steps your executor publishes, so there is no registration file to keep up to date. The paths are relative to Bench/, which is why the repository had to be cloned as anvil next to it. When Anvil.Step is on NuGet, both lines become a single PackageReference.

The entry point

Replace Bench/Program.cs with this one line:

return await Anvil.Step.StepHost.RunAsync(args, Anvil.Step.Generated.AnvilSteps.Register);

AnvilSteps.Register is the code the generator wrote. StepHost.RunAsync reads the command line, starts a server and waits for Anvil to call.

The step

Create Bench/Board.cs:

using Anvil.Step;

/// <summary>Checks on the board under test.</summary>
public static partial class Board
{
    /// <summary>Measures the supply rail, in volts.</summary>
    [Step]
    public static double MeasureRail() => 4.98;
}

That is a complete step. What each part does:

  • [Step] publishes the method.
  • The name comes from the code, in snake_case: the class Board is the module board, the method MeasureRail is the step measure_rail, and sequences call it board/measure_rail.
  • The <summary> is the step’s description. Anvil shows it, so you write it once, next to the code it describes.
  • Returning a double means “this is a measurement”. Notice what is not here: nothing says whether 4.98 V is good. That belongs to the sequence.

The class is partial only so that later chapters can add steps to the same module in files of their own.

A step can return:

returnsmeans
doublea measurement, for the sequence’s limit to judge
booltrue is pass, false is fail — the step judges, nothing is measured
voidpass, when the step is an action
Outcomeanything else: Outcome.Measured(...), Passed, Failed, Errored, with a message

Ask the executor what it serves

$ dotnet run --project Bench -- --list
life 275f5aeae20e4fc5acad7e3f418a6717, contract 4
  board/measure_rail()  — Measures the supply rail, in volts.

--list prints the catalog and exits, without starting anything. The life line identifies this run of the executor and changes every time; contract 4 is the version of the protocol Anvil and the executor speak. The first run also compiles the project, which takes a little while.

Serve it

Now start it for real, and leave this terminal open:

$ dotnet run --project Bench -- --port 9201
anvil C# executor: 1 step(s) on 127.0.0.1:9201, life bfd160da9cfc4688bd98c5eaa03a6736

The executor is listening on port 9201, only on your own machine (127.0.0.1). Anvil does not start a C# executor for you: the process is yours, so you bring it up, and you stop it with Ctrl+C.

flag
--portwhere to listen; 9201 by default
--bindwhich interface; 127.0.0.1 by default
--listprint the catalog and exit

Every time you change a step, stop the executor and start it again. The running process serves the code it was built from; dotnet run rebuilds it.

4. Your first sequence

Open a second terminal — the first one is busy serving your step — and set it up:

$ cd ~/anvil-book
$ export PATH="$PWD/anvil-v0.5.0-x86_64-linux-musl:$PATH"
$ mkdir sequences

The file

Create sequences/first.yseq:

name: first

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

main:
  - name: board/measure_rail
    executor: bench
    limit: { type: range, min: 4.75, max: 5.25 }

It is YAML. Line by line:

  • name names the sequence in reports.
  • executors is the list of executors this sequence talks to. Each has a name you choose, a typegrpc for a process listening on a port, as your C# executor is — and where to find it.
  • main is the list of steps to run. Chapter 6 adds setup and cleanup.
  • Each step has the name the executor publishes and the executor that serves it, by the name you gave it above.
  • limit is the acceptance criterion: a range passes when the measurement is between min and max.

Always write executor: on a step. A step without one is sent to a small executor built into the engine for its own demonstrations, which does not know your steps.

The extension can be .yseq or .yaml; Anvil reads both the same way.

Run it

$ anvil sequences/first.yseq
ejecutor de pasos escuchando en 40243
secuencia 'first' cargada (1 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
motor conectado
conectado a los ejecutores de pasos (embebido en 127.0.0.1:40243)
catálogo pedido
1 paso(s) comprobados contra el catálogo de su ejecutor
=== first: pass ===
  [pass] board/measure_rail: 
conexión cerrada; esperando otra
$ echo $?
0

Two kinds of lines are mixed there.

The report is the part between === first: pass === and the step lines under it: the sequence passed, and so did its one step. It goes to standard output.

Everything else is diagnostics on the error stream: the engine starting its built-in executor on a free port, loading the file, connecting, and — the line that matters — 1 paso(s) comprobados contra el catálogo de su ejecutor, “1 step checked against its executor’s catalog”. Before running anything, Anvil asked bench what it serves and checked that the sequence only asks for steps that exist. These messages are still in Spanish in 0.5.0 (#59).

The exit code is 0 because the sequence passed. Anything else — a fail, an error, a file that does not load — exits with 1.

To see only the report, send the diagnostics away:

$ anvil sequences/first.yseq 2>/dev/null
=== first: pass ===
  [pass] board/measure_rail: 

The rest of the book does that, unless the diagnostics are the point.

The console report does not show the measured value — 4.98 appears nowhere. The reports in chapter 10 carry it.

When a name is wrong

Make a typo on purpose. sequences/unknown.yseq asks for measure_rial:

name: unknown

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

main:
  - name: board/measure_rial
    executor: bench
$ anvil sequences/unknown.yseq 2>&1 | tail -n 3
la secuencia no casa con lo que ofrecen los ejecutores (1 problema(s)):
  - step 'board/measure_rial': executor 'bench' does not serve it (it serves: board/measure_rail)
conexión cerrada; esperando otra

“The sequence does not match what the executors offer”: bench does not serve board/measure_rial, and the message lists what it does serve. Nothing ran. This check happens on every run, before the first step, so a misspelt step never gets as far as the bench.

5. Measuring and judging

A step reports what it saw. The sequence says what is acceptable. Anvil compares the two. Keeping the criterion out of the step is what lets you tighten a tolerance by editing a text file that anyone can review, without rebuilding the executor.

Three more steps

Create Bench/Board.Judging.cs, which adds three steps to the board module:

using Anvil.Step;

public static partial class Board
{
    /// <summary>Measures the leakage current with the board idle, in amps.</summary>
    [Step]
    public static double MeasureLeakage() => 0.0004;

    /// <summary>Checks that the power LED is lit.</summary>
    [Step]
    public static bool CheckLed() => true;

    /// <summary>Reads the on-board temperature sensor.</summary>
    [Step]
    public static Outcome ReadTemperature() =>
        Outcome.Errored("the temperature sensor did not answer");
}

Stop the executor with Ctrl+C in its terminal and start it again with dotnet run --project Bench -- --port 9201, so it serves the new steps.

Limits

sequences/judging.yseq uses each kind of judgement:

name: judging

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

main:
  - name: board/measure_rail
    executor: bench
    limit: { type: range, min: 4.75, max: 5.25 }

  - name: board/measure_leakage
    executor: bench
    limit: { type: comparison, op: le, expected: 0.001 }

  - name: board/check_led
    executor: bench
$ anvil sequences/judging.yseq 2>/dev/null
=== judging: pass ===
  [pass] board/measure_rail: 
  [pass] board/measure_leakage: 
  [pass] board/check_led: 
  • board/measure_rail returns a number and has a range limit.
  • board/measure_leakage has a comparison: the measurement is compared with expected using op, one of eq, ne, lt, le, gt or ge. Here, “less than or equal to 1 mA”.
  • board/check_led returns a bool, so the step itself decides pass or fail and needs no limit.

Now the same rail against a limit it cannot meet, in sequences/rail-tight.yseq:

name: rail_tight

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

main:
  - name: board/measure_rail
    executor: bench
    limit: { type: range, min: 5.0, max: 5.25 }
$ anvil sequences/rail-tight.yseq 2>/dev/null
=== rail_tight: fail ===
  [fail] board/measure_rail: 4.98 fuera de rango [5, 5.25]
$ echo $?
1

The step still returned 4.98 and did not change; the verdict did. The message reads “4.98 out of range [5, 5.25]”, and the exit code is 1.

Fail is about the unit, error is about the bench

board/read_temperature does not measure anything: it answers Outcome.Errored, as a real step would when a sensor does not reply.

name: error

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

main:
  - name: board/read_temperature
    executor: bench
  - name: board/check_led
    executor: bench
$ anvil sequences/error.yseq 2>/dev/null
=== error: error ===
  [error] board/read_temperature: the temperature sensor did not answer
$ echo $?
1

The verdict is error, not fail, and the difference is deliberate. A fail is a statement about the unit — it is out of spec, reject it. An error says Anvil could not judge the unit at all: the bench is broken, the unit may be perfectly good, and treating it as rejected would scrap a good board.

So when you write a step:

  • If you measured, return the measurement and let the limit judge.
  • If the step itself can decide, return bool or Outcome.Failed.
  • If you could not measure or decide, return Outcome.Errored with a message saying why. Never return a fail to mean “something went wrong”.

An exception that escapes your method also becomes error, and the executor keeps running.

Two more things the output shows:

  • main stops at the first step that does not pass. board/check_led never ran.
  • A sequence’s verdict is the worst of its steps: any error makes it error; otherwise any fail makes it fail. Both exit with 1; the report tells them apart.

6. Setup, main, cleanup and retries

Real tests power a unit before measuring it and must power it off afterwards, whatever happened in between. Sequences have three phases for that.

Create Bench/Fixture.cs and restart the executor:

using Anvil.Step;

/// <summary>The test fixture the board sits in.</summary>
public static class Fixture
{
    /// <summary>Clamps the board and powers it.</summary>
    [Step]
    public static void PowerOn() { }

    /// <summary>Cuts power and releases the board.</summary>
    [Step]
    public static void PowerOff() { }

    /// <summary>Opens the link to the board; the first attempt always times out.</summary>
    [Step]
    public static Outcome Connect(Ctx ctx) =>
        ctx.Attempt == 1
            ? Outcome.Errored("no answer from the board")
            : Outcome.Passed("connected on attempt " + ctx.Attempt);

    /// <summary>Measures the rail while it settles: low on the first attempt.</summary>
    [Step]
    public static double MeasureSettlingRail(Ctx ctx) => ctx.Attempt == 1 ? 4.1 : 4.97;
}

Ctx is optional: declare it as a parameter and the SDK hands you the attempt number, among other things. It is not an input the sequence sends.

Three phases

name: phases

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

setup:
  - name: fixture/power_on
    executor: bench

main:
  - name: fixture/measure_settling_rail
    executor: bench
    retries: 3
    limit: { type: range, min: 4.75, max: 5.25 }

  - name: board/check_led
    executor: bench

cleanup:
  - name: fixture/power_off
    executor: bench
$ anvil sequences/phases.yseq 2>/dev/null
=== phases: fail ===
  [pass] fixture/power_on: 
  [fail] fixture/measure_settling_rail: 4.1 fuera de rango [4.75, 5.25]
  [pass] fixture/power_off: 
  • setup runs first. If any setup step does not pass, main is skipped entirely.
  • main runs next and stops at its first step that does not pass.
  • cleanup always runs, whatever happened before. fixture/power_off ran even though the measurement failed. Put everything that makes the bench safe here.

Retries do not re-measure a failed limit

That sequence has retries: 3 on fixture/measure_settling_rail, and the step reads 4.97 V from its second attempt on. Yet the report shows 4.1 and fail.

This is how 0.5.0 behaves (#76): retries repeats a step only while the step itself does not pass. The limit is applied once, after the last attempt. A step that returns a measurement has passed as far as retries are concerned, so it is not called again, and the limit then fails the first value.

Retries are for a step that fails or errors on its own — a link that times out once, for example:

name: retries

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

setup:
  - name: fixture/power_on
    executor: bench
  - name: fixture/connect
    executor: bench
    retries: 3

main:
  - name: board/measure_rail
    executor: bench
    limit: { type: range, min: 4.75, max: 5.25 }

cleanup:
  - name: fixture/power_off
    executor: bench
$ anvil sequences/retries.yseq 2>/dev/null
=== retries: pass ===
  [pass] fixture/power_on: 
  [pass] fixture/connect: connected on attempt 2
  [pass] board/measure_rail: 
  [pass] fixture/power_off: 

fixture/connect errored on attempt 1 and passed on attempt 2. retries is the total number of attempts: the default is 1, meaning no retry, and 0 is refused when the file loads.

The report keeps only the last attempt. The “on attempt 2” in the message is there because the step wrote it; Anvil does not record the earlier attempt.

Without the retries, the same link does not come up, and the setup rule shows:

name: setup_fails

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

setup:
  - name: fixture/power_on
    executor: bench
  - name: fixture/connect
    executor: bench

main:
  - name: board/measure_rail
    executor: bench
    limit: { type: range, min: 4.75, max: 5.25 }

cleanup:
  - name: fixture/power_off
    executor: bench
$ anvil sequences/setup-fails.yseq 2>/dev/null
=== setup_fails: error ===
  [pass] fixture/power_on: 
  [error] fixture/connect: no answer from the board
  [pass] fixture/power_off: 

The setup errored, main never ran, and cleanup still did.

7. Inputs, variables and flow

So far every step has taken nothing and returned a fixed value. Real steps take parameters — which channel, which range — and later steps depend on earlier results.

Create Bench/Dmm.cs and Bench/Unit.cs, then restart the executor:

using Anvil.Step;

/// <summary>A multimeter with four input channels.</summary>
public static class Dmm
{
    /// <summary>Measures the voltage on a channel.</summary>
    /// <param name="channel">The input channel, 1 to 4.</param>
    /// <param name="range">The measurement range.</param>
    [Step]
    public static Outcome MeasureVoltage(double channel, string range = "auto") =>
        channel switch
        {
            1 => Outcome.Measured(1.1, "range " + range),
            2 => Outcome.Measured(1.8, "range " + range),
            3 => Outcome.Measured(3.3, "range " + range),
            4 => Outcome.Measured(5.0, "range " + range),
            _ => Outcome.Errored("the multimeter has no such channel"),
        };
}
using Anvil.Step;

/// <summary>The unit's own identity.</summary>
public static class Unit
{
    /// <summary>Reads the serial number from the unit's memory.</summary>
    [Step]
    public static Outcome ReadSerial() =>
        Outcome.Passed("serial read").Output("serial", "SN-0042");
}

Inputs

A method’s parameters are the step’s inputs, named in snake_case like the step. channel has no default, so it is required; range has one, so it is optional. A parameter can be a double, a string, a bool, or a reference (chapter 8); anything else does not compile.

--list shows that signature — dmm/measure_voltage(channel: number, range?: text) — and Anvil checks every sequence against it.

Variables and results

name: data

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

locals:
  channel: 2
  rail: 0.0
  led_ok: false

main:
  - name: unit/read_serial
    executor: bench

  - name: dmm/measure_voltage
    executor: bench
    inputs:
      channel: '${locals.channel}'
      range: "10V"
    limit: { type: range, min: 1.7, max: 1.9 }
    assign:
      rail: result.measured_value

  - name: board/check_led
    executor: bench
    assign:
      led_ok: 'result.status == "pass"'

  - name: rail_in_spec
    type: pass_fail
    condition: 'locals.rail > 1.75 && locals.led_ok'
$ anvil sequences/data.yseq --json data.json 2>/dev/null
=== data: pass ===
  [pass] unit/read_serial: serial read
  [pass] dmm/measure_voltage: range 10V
  [pass] board/check_led: 
  [pass] rail_in_spec: condición cumplida
$ cat data.json
{
  "sequence": "data",
  "status": "pass",
  "skipped_steps": 0,
  "total_steps": 4,
  "steps": [
    {
      "name": "unit/read_serial",
      "status": "pass",
      "phase": "main",
      "message": "serial read",
      "measured_value": null,
      "limit_min": null,
      "limit_max": null,
      "expected_value": null,
      "operator": null,
      "inputs": {},
      "outputs": {
        "serial": "SN-0042"
      }
    },
    {
      "name": "dmm/measure_voltage",
      "status": "pass",
      "phase": "main",
      "message": "range 10V",
      "measured_value": 1.8,
      "limit_min": 1.7,
      "limit_max": 1.9,
      "expected_value": null,
      "operator": null,
      "inputs": {
        "channel": 2.0,
        "range": "10V"
      },
      "outputs": {}
    },
    {
      "name": "board/check_led",
      "status": "pass",
      "phase": "main",
      "message": "",
      "measured_value": null,
      "limit_min": null,
      "limit_max": null,
      "expected_value": null,
      "operator": null,
      "inputs": {},
      "outputs": {}
    },
    {
      "name": "rail_in_spec",
      "status": "pass",
      "phase": "main",
      "message": "condición cumplida",
      "measured_value": null,
      "limit_min": null,
      "limit_max": null,
      "expected_value": null,
      "operator": null,
      "inputs": {},
      "outputs": {}
    }
  ]
}

What happens there:

  • locals declares the sequence’s variables with their initial values. The type comes from the value: 2 and 0.0 are numbers, false is a boolean, "" would be text.
  • inputs gives a step its parameters. A plain value is sent as it is ("10V"); a value in ${...} is an expression, evaluated before the step runs (${locals.channel} sends 2).
  • assign stores something from the step’s result in a local after it runs: result.measured_value, or an expression such as result.status == "pass".
  • A step with type: pass_fail calls no executor. Anvil evaluates its condition and the step passes or fails on it — the place for a verdict that combines several measurements.

The JSON report (chapter 10) records what each step was given in inputs and what it returned in outputs. That is what makes a result reconstructible later: two runs on different channels produce different reports.

Outputs from a C# step cannot be assigned yet

unit/read_serial returns a named output, serial, and the report above shows it. But reading it into a variable does not work in 0.5.0:

name: serial

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

locals:
  serial: ""

main:
  - name: unit/read_serial
    executor: bench
    assign:
      serial: result.outputs.serial
$ anvil sequences/serial.yseq 2>&1 | tail -n 3
la secuencia no casa con lo que ofrecen los ejecutores (1 problema(s)):
  - step 'unit/read_serial' (bench): 'assign' reads result.outputs.serial and the step does not return it (it returns: none)
conexión cerrada; esperando otra

Anvil checks assign against the outputs a step declares in its catalog, and the C# SDK has no way yet to declare them — only the open step of chapter 8 declares one (#77). So the check refuses the sequence before it runs. Until that is fixed, use named outputs from C# for the report only, and pass values between steps through result.measured_value or result.status.

Flow

name: flow

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

file_globals:
  station: "bench-3"

locals:
  channel: 1

main:
  - name: pick_channel
    type: statement
    statement: 'locals.channel = locals.channel + 2'

  - name: dmm/measure_voltage
    executor: bench
    inputs: { channel: '${locals.channel}' }
    limit: { type: range, min: 3.0, max: 3.6 }

  - name: board/measure_leakage
    executor: bench
    precondition: 'file_globals.station == "bench-1"'
    limit: { type: comparison, op: le, expected: 0.001 }

  - name: board/read_temperature
    executor: bench
    disable: true
$ anvil sequences/flow.yseq 2>/dev/null
=== flow: pass ===
  [pass] pick_channel: statement ok
  [pass] dmm/measure_voltage: range auto
  [skipped] board/measure_leakage: precondición falsa
  [skipped] board/read_temperature: disable
  (2 de 4 pasos saltados)
  • type: statement runs an assignment inside the engine, without calling any executor: locals.channel becomes 3 before the measurement uses it.
  • file_globals are variables visible to every sequence in the file. Steps read them; writing to one is refused when the file loads.
  • precondition is an expression; when it is false the step is skipped without being called. Here the station is not bench-1.
  • disable: true skips a step without evaluating anything.

Look at the verdict: the sequence passed with two steps skipped. A skipped step is neither a pass nor a fail, and it does not fail the sequence. If a measurement must always happen, do not put a precondition on it.

Checking a sequence without running it

A typo in an input name, in sequences/typo.yseq:

name: typo

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

main:
  - name: dmm/measure_voltage
    executor: bench
    inputs: { chanel: 2 }
$ anvil sequences/typo.yseq --validate
secuencia 'typo' cargada (1 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
'typo' válida (1 paso(s) en main, 0 subsecuencia(s) externa(s))
$ anvil sequences/typo.yseq --validate --with-executors
ejecutor de pasos escuchando en 46505
secuencia 'typo' cargada (1 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
'typo' válida (1 paso(s) en main, 0 subsecuencia(s) externa(s))
motor conectado
catálogo pedido
1 paso(s) comprobados contra el catálogo de su ejecutor
la secuencia no casa con lo que ofrecen los ejecutores (2 problema(s)):
  - step 'dmm/measure_voltage' (bench): it takes no input called 'chanel' (it takes: channel, range)
  - step 'dmm/measure_voltage' (bench): the input 'channel' is required and the sequence does not send it
conexión cerrada; esperando otra

--validate loads the file and checks it — the schema, the expressions, the subsequences — without starting or connecting to anything, so it runs in CI with no bench. It cannot know what inputs a step takes, so it says the file is valid.

--validate --with-executors also asks each executor for its catalog, and catches both problems: there is no input chanel, and the required channel is missing. A real run makes the same check before its first step.

Do not treat a green --with-executors as a guarantee in 0.5.0: when an executor fails while describing itself, it still passes (#70).

8. Instruments that stay open

A session with a power supply holds a socket or a vendor driver handle. It cannot be sent to Anvil and back, and it should not be reopened for every step. So it stays inside the executor, and the sequence holds a reference to it: a handle that means something only to the executor that issued it.

In C# you write the class you would write anyway. Create Bench/PowerSupply.cs and restart the executor:

using Anvil.Step;

/// <summary>A bench power supply, kept open across several steps.</summary>
[StepModule("psu")]
public sealed class PowerSupply
{
    private double _volts;

    /// <summary>Opens the session with the supply.</summary>
    /// <param name="resource">Where the supply is.</param>
    [StepConstructor]
    public PowerSupply(string resource) => Resource = resource;

    /// <summary>Where this supply is.</summary>
    public string Resource { get; }

    /// <summary>Sets the output voltage, in volts.</summary>
    /// <param name="volts">What to set the output to.</param>
    [Step]
    public void SetVoltage(double volts) => _volts = volts;

    /// <summary>Measures the current the load draws, in amps.</summary>
    [Step]
    public double MeasureCurrent() => _volts * 0.0625;

    /// <summary>Cuts the output.</summary>
    [Step]
    public void OutputOff() => _volts = 0;
}
  • [StepConstructor] on the constructor publishes a step named openpsu/open — that creates the object, keeps it in the executor, and returns a reference to it as an output named after the module, psu.
  • [StepModule("psu")] names the module. Without it the module would be power_supply, from the class name.
  • Instance stepsSetVoltage, MeasureCurrent, OutputOff — take an extra input, psu, of type reference. The SDK finds the object it points to and calls the method on it; your method never sees the handle.

--list shows it: psu/set_voltage(psu: reference, volts: number).

Using it from a sequence

name: supply

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

locals:
  psu: { type: reference, executor: bench }

setup:
  - name: psu/open
    executor: bench
    inputs: { resource: "TCPIP::192.168.0.50::5025::SOCKET" }
    assign:
      psu: result.outputs.psu

main:
  - name: psu/set_voltage
    executor: bench
    inputs: { psu: '${locals.psu}', volts: 12.0 }

  - name: psu/measure_current
    executor: bench
    inputs: { psu: '${locals.psu}' }
    limit: { type: range, min: 0.5, max: 0.8 }

cleanup:
  - name: psu/output_off
    executor: bench
    inputs: { psu: '${locals.psu}' }
$ anvil sequences/supply.yseq --json supply.json 2>/dev/null
=== supply: pass ===
  [pass] psu/open: 
  [pass] psu/set_voltage: 
  [pass] psu/measure_current: 
  [pass] psu/output_off: 
$ grep -A 6 "\"outputs\": {$" supply.json | head -n 8
      "outputs": {
        "psu": {
          "type": "reference",
          "executor": "bench",
          "lifetime": "ad96dffbdcf9425fb91af7bf196e34d8",
          "payload": "s1"
        }
  • locals declares psu as a reference that comes from bench. It is the one kind of variable with no initial value: you cannot write a reference by hand, only receive it from a step.
  • setup opens the supply and stores the reference with assign. This is the one output a C# step can have assigned in 0.5.0 (see chapter 7).
  • main passes ${locals.psu} to each step that needs the supply.
  • cleanup turns the output off, which runs whatever happens in main.

The report records the reference like any other input or output, so you can tell afterwards which session each step used. lifetime identifies the run of the executor that issued it — the same value as the life it prints when it starts — and payload is the executor’s own name for the object.

9. Subsequences

When the same few steps appear in several places, move them into a subsequence and call it. A subsequence can live in the same file, or in a file of its own that several sequences share.

This chapter needs no new steps.

A file to share

sequences/rail-check.yseq measures one channel and hands the value back to whoever called it:

name: rail_check

parameters:
  channel: 1
  volts: 0.0

locals:
  measured: 0.0

main:
  - name: dmm/measure_voltage
    executor: bench
    inputs: { channel: '${parameters.channel}' }
    assign:
      measured: result.measured_value

  - name: return_volts
    type: statement
    statement: 'parameters.volts = locals.measured'
  • parameters are what the caller passes in, with default values.
  • It has no executors: section. The executors are declared once, in the sequence you run, and subsequences refer to them by name. Anvil refuses a subsequence that declares its own. It also means this file cannot be run on its own.
  • Returning a value takes two steps. assign always writes to a local, so the measurement goes to locals.measured, and a statement copies it to the parameter the caller will read. Assigning directly to a parameter is refused when the file loads.

Calling it

name: board

executors:
  - { name: bench, type: grpc, host: 127.0.0.1, port: 9201 }

locals:
  ch_3v3: 3
  v_3v3: 0.0
  ch_1v1: 1
  v_1v1: 0.0

subsequences:
  power_up:
    setup:
      - name: fixture/power_on
        executor: bench
    main:
      - name: board/check_led
        executor: bench

main:
  - name: power_up
    type: sequence_call
    sequence: power_up

  - name: measure_3v3
    type: sequence_call
    sequence: ./rail-check.yseq
    args: { channel: locals.ch_3v3, volts: locals.v_3v3 }

  - name: measure_1v1
    type: sequence_call
    sequence: ./rail-check.yseq
    args: { channel: locals.ch_1v1, volts: locals.v_1v1 }

  - name: rails_in_spec
    type: pass_fail
    condition: 'locals.v_3v3 > 3.2 && locals.v_3v3 < 3.4 && locals.v_1v1 > 1.0 && locals.v_1v1 < 1.2'

cleanup:
  - name: fixture/power_off
    executor: bench
$ anvil sequences/board.yseq 2>/dev/null
=== board: pass ===
  [pass] power_up: sequence call 'power_up' → pass
    [pass] fixture/power_on: 
    [pass] board/check_led: 
  [pass] measure_3v3: sequence call 'sequences/rail-check.yseq' → pass
    [pass] dmm/measure_voltage: range auto
    [pass] return_volts: statement ok
  [pass] measure_1v1: sequence call 'sequences/rail-check.yseq' → pass
    [pass] dmm/measure_voltage: range auto
    [pass] return_volts: statement ok
  [pass] rails_in_spec: condición cumplida
  [pass] fixture/power_off: 
  • subsequences holds power_up, a subsequence that lives in this file. It has its own setup and main.
  • A step with type: sequence_call calls one. sequence is either the name of a subsequence in the same file (power_up) or a path to a file, relative to the file doing the calling (./rail-check.yseq).
  • args connects each parameter to one of the caller’s locals — channel: locals.ch_3v3. It is written without ${...} because it is not a value: it names the variable, and when the subsequence writes the parameter, the caller’s local changes. That is how locals.v_3v3 gets its value.

The report nests each subsequence’s steps under the call — indented on the console, under sub_steps in the JSON report — and the call takes the verdict of the subsequence: if a step inside errors, the call is an error too, and the calling sequence stops as it would for any other step.

10. Reports and running unattended

On a production line nobody reads the console. This chapter is about the files Anvil leaves behind and the options for running without a person in front of it.

Reports

--json <file> and --csv <file> write the report to a file, in addition to the console. --quiet removes the console report:

$ anvil sequences/judging.yseq --quiet --csv judging.csv
ejecutor de pasos escuchando en 46037
motor conectado
catálogo pedido
conexión cerrada; esperando otra
$ cat judging.csv
sequence_name,status,step_name,step_status,message,measured_value,limit_min,limit_max,expected_value,operator,phase,inputs,outputs
judging,pass,board/measure_rail,pass,,4.98,4.75,5.25,,,main,,
judging,pass,board/measure_leakage,pass,,0.0004,,,0.001,<=,main,,
judging,pass,board/check_led,pass,,,,,,,main,,

One row per step. Unlike the console, the files carry the measurement and the limit it was judged against: measured_value with limit_min and limit_max for a range, expected_value and operator for a comparison. They also carry the step’s inputs and outputs, and a subsequence’s steps are prefixed with the name of their call. The JSON report has the same information, nested (chapter 7 showed one).

Notice that --quiet did not silence the diagnostics on the error stream (#35). If a script needs silence, redirect it.

Changing limits without touching the sequence

A tolerance often depends on the product variant or the production lot, not on the test. --limits loads a limits file that replaces the limits written in the sequence, by step name. sequences/judging.limits.yaml:

board/measure_rail:
  type: range
  min: 5.0
  max: 5.1
$ anvil sequences/judging.yseq --limits sequences/judging.limits.yaml 2>/dev/null
=== judging: fail ===
  [fail] board/measure_rail: 4.98 fuera de rango [5, 5.1]

The same sequence, a tighter tolerance, a different verdict. The limits file applies to every step with that name, in subsequences too.

A name that matches no step would leave the sequence’s own limit in force without anyone noticing, so Anvil warns — even under --quiet. With sequences/judging.typo.limits.yaml:

board/measure_rial:
  type: range
  min: 5.0
  max: 5.1
$ anvil sequences/judging.yseq --limits sequences/judging.typo.limits.yaml
ejecutor de pasos escuchando en 40627
secuencia 'judging' cargada (3 pasos en main, 0 subsecuencia(s) externa(s), 1 ejecutor(es))
sidecar de límites 'sequences/judging.typo.limits.yaml' aplicado (0 paso(s) afectado(s))
aviso: 1 límite(s) del sidecar 'sequences/judging.typo.limits.yaml' no afectan a ningún paso: board/measure_rial
aviso: el sidecar no afectó a ningún paso. Comprueba que los nombres coincidan con los de los pasos de la secuencia
motor conectado
conectado a los ejecutores de pasos (embebido en 127.0.0.1:40627)
catálogo pedido
3 paso(s) comprobados contra el catálogo de su ejecutor
=== judging: pass ===
  [pass] board/measure_rail: 
  [pass] board/measure_leakage: 
  [pass] board/check_led: 
conexión cerrada; esperando otra

The aviso lines say that one limit in the file affects no step, board/measure_rial, and that the file changed nothing: check the names. The sequence passed against its own limit.

Moving an executor without editing the sequence

--executor name=host:port points a declared executor somewhere else for this run — the same sequence against the executor on your desk or on the line’s PC:

$ anvil sequences/first.yseq --executor bench=127.0.0.1:9201

The name must be one the sequence declares; any other is an error when the file loads. It can be repeated, once per executor.

Checking in CI

$ anvil sequences/board.yseq --validate
secuencia 'board' cargada (4 pasos en main, 1 subsecuencia(s) externa(s), 1 ejecutor(es))
'board' válida (4 paso(s) en main, 1 subsecuencia(s) externa(s))
$ echo $?
0

--validate needs no executor and no bench, and exits with 0 when the file is valid, so it can guard every change to a sequence in CI. Remember from chapter 7 what it cannot see: whether steps and their inputs exist. For that, --validate --with-executors with the executors running — and, in 0.5.0, with #70 in mind.

Exit codes

0 when the sequence passes. 1 for everything else: a fail, an error, a file that does not load, a sequence that does not match its executors, an executor that cannot be reached. A script can stop on the exit code, and read the report to know which of those it was.

Following a run as it happens

--events writes one JSON line per event — a step starting, a step’s result — to the error stream while the sequence runs. It is what the Sequence Editor reads to light up the running step. --quiet does not silence it, and it carries the same data as --json, instrument addresses included.

Not covered here

--process-model wraps a sequence in a process model that identifies the unit and reports on it. The only process model in 0.5.0 relies on the engine’s built-in demonstration steps, and it is not in the release archive, so this book leaves it out.

12. Steps in Python and Rust

Everything from chapter 4 on is about the sequence, and the sequence does not know what language a step is written in. It names the step and the executor. To show that, this chapter writes board/measure_rail twice more — in Python and in Rust — and runs it with a sequence that differs from sequences/first.yseq only in its executors: line.

Writing the step is much the same in all three languages: the function’s signature is the catalog, a returned number is a measurement, and the limit lives in the sequence. What differs is how the executor is started, and that is what this chapter is about.

C#PythonRust
the executor isyour own programa server from the repository, pointed at a folderanvil-exec-wasm, next to your compiled modules
who starts ityouyouAnvil, when a sequence uses it
executor typegrpcgrpcwasm
a step isa method with [Step]a function with @stepa function with #[step]

Python

The Python executor is not in the release archive: it is in the repository you cloned in chapter 2. It needs grpcio, and the gRPC code it imports has to be generated once after cloning. From ~/anvil-book:

$ python3 -m venv .venv
$ .venv/bin/pip install grpcio grpcio-tools
$ cd anvil/executors/python
$ ../../../.venv/bin/python -m grpc_tools.protoc -I ../../crates/modelo --python_out=. --grpc_python_out=. ../../crates/modelo/paso.proto
$ cd ~/anvil-book

If python3 -m venv fails, your distribution may ship it as a separate package — python3-venv on Debian and Ubuntu. That case is not verified in this book.

A step is a decorated function. Create python_steps/board.py:

from anvil_step import step


@step
def measure_rail() -> float:
    """Measures the supply rail, in volts."""
    return 4.98

The file name is the module, so this is board/measure_rail again. Returning a float is a measurement, a bool is pass or fail, and Result.error(...) reports what could not be judged, as in C#. An exception becomes error.

Start the executor in its own terminal and leave it running:

$ .venv/bin/python anvil/executors/python/anvil-exec-python --steps python_steps
module 'board' (/home/you/anvil-book/python_steps/board.py) sha256:831270a42ae7bb6a050f189a2dcf434cf8cc44ca68122871a74e84f3808009e6
python executor listening on 127.0.0.1:9101 — 1 step(s) from 1 module(s): board/measure_rail

It serves every module in the folder given to --steps, on port 9101 by default. You never edit the server; to add steps, add files to the folder and restart it.

name: python

executors:
  - { name: py, type: grpc, host: 127.0.0.1, port: 9101 }

main:
  - name: board/measure_rail
    executor: py
    limit: { type: range, min: 4.75, max: 5.25 }
$ anvil sequences/python.yseq 2>/dev/null
=== python: pass ===
  [pass] board/measure_rail: 

The Python executor’s own README, executors/python/README.md, covers the rest: named outputs, references and options. It is still in Spanish.

Rust

Rust steps are compiled to WebAssembly modules, and the executor is a program that ships in the release archive, anvil-exec-wasm. It serves every .wasm file in the folder it sits in. You need a Rust toolchain and one extra target:

$ rustup target add wasm32-wasip2

Create board-wasm/Cargo.toml — the package name is the module name:

[package]
name = "board"
version = "0.1.0"
edition = "2021"

[dependencies]
anvil-step = { path = "../anvil/executors/rust/anvil-step" }

[lib]
crate-type = ["cdylib"]

and board-wasm/src/lib.rs:

#![allow(unused)]
fn main() {
use anvil_step::{step, Outcome};

/// Measures the supply rail, in volts.
#[step]
fn measure_rail() -> Outcome {
    Outcome::measured(4.98)
}

anvil_step::export!();
}

Build it, and put the module next to a copy of the executor. That folder is the executor, and you can copy it to another machine as it is:

$ cargo build --target wasm32-wasip2 --manifest-path board-wasm/Cargo.toml
$ mkdir wasm-dept
$ cp anvil-v0.5.0-x86_64-linux-musl/anvil-exec-wasm board-wasm/target/wasm32-wasip2/debug/board.wasm wasm-dept/

The first build takes about a minute. Then:

name: wasm

executors:
  - { name: rs, type: wasm, path: ../wasm-dept/anvil-exec-wasm }

main:
  - name: board/measure_rail
    executor: rs
    limit: { type: range, min: 4.75, max: 5.25 }
$ anvil sequences/wasm.yseq 2>/dev/null
=== wasm: pass ===
  [pass] board/measure_rail: 
$ ./wasm-dept/anvil-exec-wasm --list
board  sha256:2087caa515293f3a0cda2b14c78a09d6d9b2bfb98a5e2c63916c872cb1d807d5
    /home/you/anvil-book/wasm-dept/board.wasm
    board/measure_rail()
        Measures the supply rail, in volts.

There was no executor to start: with type: wasm, Anvil starts anvil-exec-wasm itself from path. That path is relative to the sequence file, not to the folder you run anvil from — hence the ../. anvil-exec-wasm --list shows each module with the SHA-256 of the file that serves it.

The step-by-step Rust quick start goes further, with two modules and optional inputs, and executors/rust/README.md covers the SDK.

13. When something goes wrong

The executor is not running

The most common mistake: you run a sequence and forgot to start the executor in the other terminal. In 0.5.0 Anvil does not say so clearly. It retries the connection for several seconds, printing hundreds of lines of motor conectado and conexión cerrada; esperando otra (“engine connected”, “connection closed; waiting for another”), and then ends with:

$ anvil sequences/first.yseq 2>&1 | tail -n 2
conexión cerrada; esperando otra
no se pudo conectar a los ejecutores de pasos (embebido en 127.0.0.1:33973): WASI socket error: ErrorCode { code: 14, name: "connection-refused", message: "The TCP connection was forcefully rejected" }

“Could not connect to the step executors … connection refused”. Two things in that message are misleading. It names embebido, the engine’s built-in executor, and a port that is not the one you declared — but the executor that refused is yours, bench on 9201 (#78). When you see connection-refused, check first that every grpc executor in the sequence is running:

$ ss -ltn | grep 9201

prints a line when something is listening on that port, and nothing when not.

An old executor is still running

The opposite problem is worse, because nothing fails. If an executor from an earlier session is still listening on the port — started in a terminal you closed, or left behind by a crash — Anvil talks to it, and it answers with whatever code it was built from. You change a step, run the sequence, and see the old behaviour.

Before chasing a result that makes no sense, see who owns the port:

$ ss -ltnp | grep 9201

The process name and number are at the end of the line; stop it and start your executor again.

If you start a C# executor while another one holds the port, it prints its usual anvil C# executor: … on 127.0.0.1:9201 line and then stops with Unhandled exception. System.IO.IOException: Failed to bind to address http://127.0.0.1:9201: address already in use. and a stack trace. Believe the exception, not the first line (#79).

I changed a step and nothing changed

The executor serves the code it was started with. Stop it and start it again; dotnet run rebuilds. The same goes for the Python executor. Rust modules are read when Anvil starts the executor, so rebuild and copy the .wasm again.

The sequence does not match the executors

la secuencia no casa con lo que ofrecen los ejecutores is Anvil refusing to run because the sequence asks for something an executor does not offer. The lines below it are in English and say exactly what: a step the executor does not serve (chapter 4), an input it does not take or a required one missing (chapter 7), an output it does not return (chapter 7). Nothing ran.

Messages that name Spanish keys

Some load errors in 0.5.0 still name the Spanish spelling of a key — for instance, a subsequence with its own executors: section is told to refer to executors “by name with ejecutor:”. The key is executor:. The sequence format itself is English only (#59).

The AppImage does not start

dlopen(): error loading libfuse.so.2 means FUSE 2 is not installed; see the end of chapter 2.