Steps are the fundamental building blocks of execution in IdLE. They encapsulate domain logic and perform concrete actions during the execution of a lifecycle plan.
This document defines the conceptual model of a step, the meaning of step metadata, and the expectations placed on step implementations. It serves as a normative reference for step authors and reviewers.
A step is a self-contained unit of work executed as part of a plan.
A step:
- performs a single, well-defined responsibility
- operates on the execution context provided by the engine
- may interact with external systems through providers
- reports its outcome through status and events
Steps do not orchestrate other steps and do not control execution flow beyond their own outcome.
Steps should be:
- idempotent (converge towards the desired state)
- deterministic (same inputs produce the same plan)
- provider-agnostic (use provider contracts, not direct system calls)
- safe for preview (planning must not change external state)
During execution, each step follows a consistent lifecycle:
- The engine signals the start of the step
- The step evaluates its own applicability (if applicable)
- The step performs its domain logic
- The step reports its result and any relevant events
The engine is responsible for invoking steps and sequencing their execution.
Metadata describes what a step is, not how it is implemented.
Typical conceptual metadata includes:
- Name
- Human-readable identifier
- Purpose
- Description of what the step is intended to achieve
- Idempotency
- Whether repeated execution leads to the same state
- Inputs
- Expected configuration values or request data
- Outputs
- State changes or information produced
- Side effects
- External systems affected by the step
Every step type registered with the engine must declare a data-only metadata entry containing:
RequiredCapabilities— capability identifiers the step requires from providersWithSchema— declares theWith.*key contract used for plan-time validation
'IdLE.Step.Example' = @{
RequiredCapabilities = @('Some.Capability')
WithSchema = @{
RequiredKeys = @('IdentityKey')
OptionalKeys = @('Provider', 'AuthSessionName', 'AuthSessionOptions')
}
}WithSchema (mandatory):
RequiredKeys— keys that must be present inWith(plan creation fails if any are missing).OptionalKeys— keys that may be present inWith. Any key not inRequiredKeysorOptionalKeyscauses a plan-creation error (fail-fast, with step name, type, and offending key in the message).
Rules:
- Both
RequiredKeysandOptionalKeysmust be non-null string arrays (may be empty:@()). - A key must not appear in both sets.
- Metadata must be data-only — ScriptBlocks are rejected.
Step pack modules expose their catalog via Get-IdleStepMetadataCatalog, which loads from a
StepMetadataCatalog.psd1 data file. Host-supplied step types may supplement (but not override)
the catalog via Providers.StepMetadata.
Metadata exists to make steps:
- understandable
- reviewable
- documentable
Steps should be designed to be idempotent whenever possible.
Idempotent steps:
- can be executed multiple times without unintended side effects
- are safer in retries and error recovery scenarios
- improve predictability of plans
If a step is not idempotent, this must be clearly documented in its metadata.
Steps are the only components allowed to produce side effects.
However:
- side effects must be explicit and intentional
- steps must not perform hidden or implicit changes
- external interactions must be delegated to providers
This keeps side effects observable and testable.
When writing a new step, authors should:
- keep the responsibility narrow and focused
- rely only on documented provider contracts
- emit meaningful events for observability
- respect idempotency expectations
- avoid embedding configuration or environment assumptions
Configuration supplies parameters to steps. It must not replace step logic.
Steps should:
- interpret configuration declaratively
- validate required inputs
- remain functional across environments
Steps interact with external systems exclusively through providers.
Steps must not:
- access infrastructure directly
- assume provider implementation details
- embed credentials or environment-specific values
Steps receive inputs from the workflow under Inputs and may reference:
Request.*State.*Policy.*(optional root, host-defined)
All step inputs must be data-only and must not contain ScriptBlocks.
Step implementations MUST validate their inputs using the centralized helper:
Assert-IdleNoScriptBlock -InputObject $config -Path 'With.Config'The Assert-IdleNoScriptBlock function is exported from IdLE.Core and recursively validates hashtables, arrays, and PSCustomObjects.
Do not implement custom ScriptBlock validation. Use the centralized helper to ensure consistent enforcement across all steps.
Steps may write to State.* only, and only to declared output paths.
This prevents hidden coupling between steps.
Steps may emit structured events for progress and audit.
The engine provides a stable, object-based contract on the execution context:
Context.EventSink.WriteEvent(Type, Message, StepName, Data)
Notes:
Typeis a short string (for example:Custom,Debug).Messageis a human-readable message.StepNameshould be the current step name (if available).Datais an optional hashtable for structured details.
Example:
$Context.EventSink.WriteEvent(
'Custom',
'Ensured Department attribute.',
$Step.Name,
@{ Provider = 'Identity'; Attribute = 'Department' }
)- Steps must never execute code from configuration.
- Steps must not assume a specific host UI.
- Hosts can optionally stream events via
Invoke-IdlePlan -EventSink <object>, but ScriptBlock sinks are not supported.
IdLE uses a fail-fast execution model for primary workflow steps:
- A failing step stops plan execution immediately
- Subsequent primary steps are not executed
- Results and events capture what happened up to the failure
When primary steps fail, workflows can define OnFailureSteps for cleanup or rollback.
OnFailureSteps are executed in best-effort mode:
- Each OnFailure step is attempted regardless of previous OnFailure step failures
- OnFailure step failures do not stop execution of remaining OnFailure steps
- The overall execution status remains 'Failed' even if all OnFailure steps succeed
Execution result structure:
$result.Status # 'Failed' when primary steps fail
$result.Steps # Array of primary step results (only executed steps)
$result.OnFailure.Status # 'NotRun', 'Completed', or 'PartiallyFailed'
$result.OnFailure.Steps # Array of OnFailure step resultsOnFailure status values:
NotRun: No primary steps failed, OnFailure steps were not executedCompleted: All OnFailure steps succeededPartiallyFailed: At least one OnFailure step failed, but execution continued
For details on declaring OnFailureSteps, see Workflows.
A step may produce a Blocked outcome when a runtime precondition fails at execution time.
Blocked is a first-class outcome distinct from Failed:
Blockedrepresents a policy/safety gate, not a technical error.- Execution stops immediately (subsequent steps are not run).
OnFailureStepsdo not run for aBlockedoutcome.result.Statusis'Blocked';result.OnFailure.Statusis'NotRun'.
For details on configuring runtime preconditions, see Runtime Preconditions.
Steps that attempt to do too much:
- become hard to test
- blur responsibility boundaries
- reduce reuse
Prefer composing multiple focused steps instead.
Complex logic does not belong in configuration.
If a workflow becomes hard to read, the logic likely belongs in a step.
Undocumented behavior leads to:
- fragile steps
- unclear reviews
- broken expectations
Metadata is not optional; it is part of the step contract.