Skip to content

Method TOML

Audience: authors and operators building methods. Scope: every step kind a .method.toml can carry, how the MethodExecutor walks them, and the cross-validation against the active hardware profile.


What a method is

A method is a typed segmented profile — a list of steps the recipe runner procedure walks at run-time. Methods live on disk as *.method.toml files under configs/methods/, and a single method file is reusable across hardware profiles as long as the channel names line up.

A method is declarative. The recipe runner does not need to know anything specific about a method; new control logic that cannot be expressed as a list of steps belongs in a custom procedure, not in a richer method format.

Methods are optional. Procedures with uses_method = False (Free run, Heat-flux tune) disable the Method tab entirely.

Top-level shape

name = "sim_capa_pyrolysis"
description = "Heat-up + soak under N2 purge, then safe shutdown."

[[steps]]
# ... first step

[[steps]]
# ... second step
Field Type Required Notes
name str yes Stable identifier; surfaced in events and the manifest.
description str no Free-text. Defaults to empty.
steps array of tables yes At least one step. Discriminated by kind.

The shipped CAPA simulator method is short but exercises the two most-common step kinds:

configs/methods/sim_capa_pyrolysis.method.toml
name = "sim_capa_pyrolysis"
description = "Heat-up + soak under N2 purge, then safe shutdown."

[[steps]]
kind = "hold"
value = 600.0
duration_s = 600.0
safety_overrides = []
[steps.target]
name = "heater.setpoint"

[[steps]]
kind = "safe_shutdown"
duration_s = 0.0
safety_overrides = []
[steps.cool_target]
"heater.setpoint" = 20.0
"purge.flow" = 0.0

Step variants

The eight step variants live in capa.experiment.method. The kind field is the discriminator. Pydantic refuses any value not in this list.

Kind Drives a setpoint? Waits? Typical use
hold yes duration or condition command a value, hold for a time
ramp yes (linear) duration drive a setpoint linearly to a new value
setpoint yes no command a value, do not wait
wait no duration or condition dwell
prompt no until operator clears "ignite specimen, then Continue"
acquire no duration record without changing outputs
safe_shutdown yes duration explicit cooldown step
custom plugin-defined plugin-defined dispatch to a registered handler

Common fields available on every variant:

Field Notes
notes Free-text comment carried into events / bundle.
safety_overrides Array of AlarmOverride tables. Parsed and preserved today; active enforcement is reserved for the safety monitor path.

hold

Command a value and wait for a duration or an end condition. At least one of duration_s and end_condition must be set; both may be set (first-to-fire wins).

[[steps]]
kind = "hold"
value = 600.0
duration_s = 600.0
[steps.target]
name = "heater.setpoint"
Field Required Notes
target.name yes Channel to command. Must resolve to a declared channel.
value yes Setpoint value (channel-unit).
duration_s conditional Hold duration. Required unless end_condition is set.
end_condition conditional End the step when the condition fires.

End conditions use a simple op-and-value shape:

[steps.end_condition]
channel = "mass_loss_fraction"
op = ">"
value = 0.1

Operators: >, >=, <, <=, ==.

ramp

Drive a setpoint linearly. At least one of rate_per_second or duration_s must be set. If both are present, duration_s controls execution; if duration_s is absent, capa derives it from rate_per_second and the endpoints.

[[steps]]
kind = "ramp"
end_value = 600.0
duration_s = 300.0
[steps.target]
name = "heater.setpoint"
Field Required Notes
target.name yes Channel to command.
start_value no None = start from the current setpoint.
end_value yes Target setpoint.
rate_per_second conditional Used when duration_s is unset.
duration_s conditional Execution duration. Takes precedence when both fields are present.

setpoint

Command an immediate setpoint change and continue without waiting. Useful for staging multiple commands inside a fast sequence where the next hold or wait provides the dwell.

[[steps]]
kind = "setpoint"
value = 100.0
[steps.target]
name = "purge.flow"

wait

Dwell — no setpoint change. Either duration_s or end_condition must be set. timeout_s is an engine-side deadline used with end_condition; on timeout, on_timeout decides what to do.

[[steps]]
kind = "wait"
duration_s = 30.0
[[steps]]
kind = "wait"
timeout_s = 120.0
on_timeout = "warn"
[steps.end_condition]
channel = "balance.stable"
op = "=="
value = 1.0
Field Required Notes
duration_s conditional Set this or end_condition.
end_condition conditional Set this or duration_s.
timeout_s no Deadline for end_condition.
on_timeout no "warn" (default), "abort", or "safe_shutdown".

prompt

Block until the operator acknowledges. The headless RecipeRunner config can set auto_acknowledge_prompts = true to skip without operator interaction — common for batch and CI runs.

[[steps]]
kind = "prompt"
title = "Ignite specimen"
message = "Apply spark for 3 seconds, then click Continue."
timeout_s = 60.0
Field Required Notes
title no Dialog title. Defaults to "Operator confirmation".
message yes Body text.
timeout_s no If the operator does not respond by this deadline, the procedure raises.

acquire

Record without changing any control outputs. The step exists so a method can mark "this segment is the measurement window" without modifying setpoints.

[[steps]]
kind = "acquire"
duration_s = 600.0

safe_shutdown

Reusable cooldown step. Procedures can include it directly or run it from their own cleanup path with MethodExecutor.run_segment(...). cool_target is a map of channel_name → setpoint to drive during shutdown.

[[steps]]
kind = "safe_shutdown"
duration_s = 0.0
[steps.cool_target]
"heater.setpoint" = 20.0
"purge.flow" = 0.0
Field Required Notes
cool_target no Channel → setpoint map. Defaults to {}.
duration_s no Cooldown dwell after commanding cool_target.

External stops do not automatically skip to a trailing safe_shutdown step. If a procedure needs that behavior, it must invoke the cleanup step explicitly. See Shutdown sequence.

custom

Plugin-defined step. Dispatched at runtime to a handler keyed by handler_id. The plugin's Pydantic config schema validates params once the executor loads the plugin.

[[steps]]
kind = "custom"
handler_id = "my_lab.purge_check"
[steps.params]
target_flow_sccm = 100.0
tolerance_sccm = 5.0
window_s = 10.0

See Custom method steps for writing one.

safety_overrides — reserved alarm tweaks

Every step accepts a safety_overrides array of AlarmOverride tables. The schema exists so methods can carry intended per-step alarm tweaks, but the current runtime does not yet apply these fields to an active safety monitor. The intended future use is widening an alarm band during a spike at the start of a fresh setpoint:

[[steps]]
kind = "hold"
value = 800.0
duration_s = 300.0
[steps.target]
name = "heater.setpoint"

[[steps.safety_overrides]]
alarm_id = "heater_overtemp"
threshold = 850.0   # widen from default 820 for this step
Field Required Notes
alarm_id yes Alarm identifier to override when the safety monitor path lands.
threshold no Override value.
disable no Intended to disable the band for the duration of this step.

total_duration_s — when it's known

The method's total_duration_s() sums every step's duration_s when all steps have one. It returns None when at least one step has no fixed duration — a hold with only an end_condition, a wait with no time bound, a prompt that the operator clears manually, a safe_shutdown with no dwell.

The disk-space preflight uses this for the cameras' estimated_bps projection. When the duration is unknowable, the preflight falls back to a configured default rather than refusing to arm.

Cross-validation with the hardware profile

Loading a method against an ExperimentConfig triggers one extra check beyond per-step validation: every step's target.name must resolve to a declared channel in the active hardware profile.

hardware.channels = ["heater.setpoint", "heater.pv", "purge.flow", ...]

method step:
    target.name = "heater.setpoint"   ✓ resolves
    target.name = "heater_setpt"      ✗ rejected

A setpoint mismatch fails fast at apply time, not at the point where the conductor would otherwise issue the command mid-run.

Worked example: ramp + soak

name = "ramp_then_soak"
description = "Ramp heater at 10 °C/min to 600 °C, soak 10 min, cool down."

[[steps]]
kind = "ramp"
end_value = 600.0
rate_per_second = 0.1667     # 10 °C / min
[steps.target]
name = "heater.setpoint"

[[steps]]
kind = "hold"
value = 600.0
duration_s = 600.0
[steps.target]
name = "heater.setpoint"

[[steps]]
kind = "safe_shutdown"
duration_s = 60.0
[steps.cool_target]
"heater.setpoint" = 20.0
"purge.flow" = 0.0

See also