Steps
Steps are units of work that will be executed at runtime. Different step types exist for different purposes.
Types of Steps
Rust Steps
Rust steps execute Rust code at runtime and are the most common step type in flowey.
emit_rust_step: The primary method for emitting steps that run Rust code. Steps can claim variables, read inputs, perform work, and write outputs. Returns an optional ReadVar<SideEffect> that other steps can use as a dependency.
emit_minor_rust_step: Similar to emit_rust_step but for steps that cannot fail (no Result return) and don't need visibility in CI logs. Used for simple transformations and glue logic. Using minor steps also improve performance, since there is a slight cost to starting and ending a 'step' in GitHub and ADO. During the build stage, minor steps that are adjacent to each other will get merged into one giant CI step.
emit_rust_stepv: Convenience method that combines creating a new variable and emitting a step in one call. The step's return value is automatically written to the new variable.
For detailed examples of Rust steps, see the NodeCtx emit methods documentation.
ADO Steps
emit_ado_step: Emits a step that generates Azure DevOps Pipeline YAML. Takes a closure that returns a YAML string snippet which is interpolated into the generated pipeline.
For ADO step examples, see the NodeCtx::emit_ado_step documentation.
GitHub Steps
emit_gh_step: Creates a GitHub Actions step using the fluent GhStepBuilder API. Supports specifying the action, parameters, outputs, dependencies, and permissions. Returns a builder that must be finalized with .finish(ctx).
For GitHub step examples, see the GhStepBuilder documentation.
Side Effect Steps
emit_side_effect_step: Creates a dependency relationship without executing code. Useful for aggregating multiple side effect dependencies into a single side effect. More efficient than emitting an empty Rust step.
For side effect step examples, see the NodeCtx::emit_side_effect_step documentation.
Isolated Working Directories and Path Immutability
Each step gets its own fresh local working directory. This avoids the "single global working directory dumping ground" common in bash + YAML systems.
However, while flowey variables enforce sharing XOR mutability at the type-system level, developers must manually enforce this at the filesystem level:
Steps must NEVER modify the contents of paths referenced by ReadVar<PathBuf>.
When you write a path to WriteVar<PathBuf>, you're creating an immutable contract. Other steps reading that path must treat it as read-only. If you need to modify files from a ReadVar<PathBuf>, copy them to your step's working directory.
Runtime Services
Runtime services provide the API available during step execution (inside the
closures passed to emit_rust_step, etc.).
RustRuntimeServices
RustRuntimeServices is the primary runtime service available in Rust steps. It provides:
Variable Operations
- Reading and writing flowey variables
- Secret handling (automatic secret propagation for safety)
- Support for reading values of any type that implements
ReadVarValue
Environment Queries
- Backend identification (Local, ADO, or GitHub)
- Platform detection (Windows, Linux, macOS)
- Architecture information (x86_64, Aarch64)
AdoStepServices
AdoStepServices provides integration with Azure DevOps-specific features when emitting ADO YAML steps:
ADO Variable Bridge:
- Convert ADO runtime variables (like
BUILD.SOURCEBRANCH) into flowey vars - Convert flowey vars back into ADO variables for use in YAML
- Handle secret variables appropriately
Repository Resources:
- Resolve repository IDs declared as pipeline resources
- Access repository information in ADO-specific steps
GhStepBuilder
GhStepBuilder is a fluent builder for constructing GitHub Actions steps with:
Step Configuration:
- Specifying the action to use (e.g.,
actions/checkout@v4) - Adding input parameters via
.with() - Capturing step outputs into flowey variables
- Setting conditional execution based on variables
Dependency Management:
- Declaring side-effect dependencies via
.run_after() - Ensuring steps run in the correct order
Permissions:
- Declaring required GITHUB_TOKEN permissions
- Automatic permission aggregation at the job level
Secret Variables and CI Backend Integration
Flowey provides built-in support for handling sensitive data like API keys, tokens, and credentials through secret variables. Secret variables are treated specially to prevent accidental exposure in logs and CI outputs.
How Secret Handling Works
When a variable is marked as secret, flowey ensures:
- The value is not logged or printed in step output
- CI backends (ADO, GitHub Actions) are instructed to mask the value in their logs
- Secret status is automatically propagated to prevent leaks
Automatic Secret Propagation
To prevent accidental leaks, flowey uses conservative automatic secret propagation:
If a step reads a secret value, all subsequent writes from that step are automatically marked as secret by default. This prevents accidentally leaking secrets through derived values.
For example:
#![allow(unused)] fn main() { ctx.emit_rust_step("process token", |ctx| { let secret_token = secret_token.claim(ctx); let output_var = output_var.claim(ctx); |rt| { let token = rt.read(secret_token); // Reading a secret // This write is AUTOMATICALLY marked as secret // (even though we're just writing "done") rt.write(output_var, &"done".to_string()); Ok(()) } }); }
If you need to write non-secret data after reading a secret, use write_not_secret():
#![allow(unused)] fn main() { rt.write_not_secret(output_var, &"done".to_string()); }
Best Practices for Secrets
- Never use
ReadVar::from_static()for secrets - static values are encoded in plain text in the generated YAML - Always use
write_secret()when writing sensitive data like tokens, passwords, or keys - Minimize secret lifetime - read secrets as late as possible and don't pass them through more variables than necessary