Pure and Idempotent Function Blocks
Every function block declares two boolean flags — is_pure and is_idempotent
— that tell the workflow engine how the block behaves with respect to side
effects. These flags drive retry logic, failure classification, and rollback
decisions across the entire workflow.
Why it matters
When a workflow step fails, the engine must decide: is it safe to retry?
If every completed step was read-only, no device state changed — retry from
scratch. If a step pushed a configuration and the next step failed, retrying
blindly might apply that configuration twice. is_pure and is_idempotent
give the engine the information it needs to make that decision automatically.
Pure function blocks
A pure function block has no side effects. It reads state but never modifies it — not on devices, not in the CMS, not in external systems.
Examples:
- Collecting device facts via
show version - Reading running configuration for backup
- Checking compliance against a policy
- Computing a diff between intended and actual state
Pure blocks are safe to retry, cache, and skip without consequence. The engine can re-execute them at any time and get the same result (assuming device state has not changed externally).
FAILED_SAFE vs FAILED_UNSAFE
When a workflow fails, the engine inspects all steps that completed before the
failure point. If every one of them is marked is_pure=True, the workflow
transitions to FAILED_SAFE — indicating that no side effects occurred and
the entire workflow can be retried safely.
If any completed step is not pure, the workflow transitions to
FAILED_UNSAFE, signaling that manual inspection may be required before
retrying.
Idempotent function blocks
An idempotent function block can be executed multiple times and produce the same outcome each time. It may have side effects, but those side effects converge to a single state.
Examples:
- Pushing a configuration template (applying the same config twice results in the same device state)
- Setting a hostname (the value is the same regardless of how many times you set it)
- Creating entities with
skip_if_identifier_exists=True(duplicates are skipped on re-runs)
Idempotent blocks are safe to retry individually. If the block itself fails partway through, the engine can re-execute it without creating inconsistency.
Decision table
Use this table to classify your function blocks:
| Task | is_pure |
is_idempotent |
Reasoning |
|---|---|---|---|
| Config backup | True |
True |
Reads config, stores as fact. No device state change. |
| Compliance check | True |
True |
Compares state against policy. Read-only. |
| Collect interface counters | True |
True |
Reads operational data. No mutation. |
| Push config template | False |
True |
Changes device state, but same config applied each time converges. |
| Set hostname | False |
True |
Writes to device, but result is the same on repeat. |
| Device discovery with dedup | False |
True |
Creates entities, but skip_if_identifier_exists prevents duplicates. |
| Send Slack notification | False |
False |
Side effect (message sent), not idempotent (sends again on retry). |
| Trigger external CI pipeline | False |
False |
External side effect with no convergence guarantee. |
| Increment a counter | False |
False |
Each execution changes the result. |
Note
A block that is is_pure=True is implicitly idempotent — a function with no
side effects always produces the same outcome. You can set both flags to
True for clarity, but is_pure=True alone is sufficient for the engine's
retry logic.
Registration example
The config backup example demonstrates a pure, idempotent function block:
@register_function_block(
Registration(
name="configBackup",
description="Back up the running configuration of a network device.",
package="fb.examples.neops.io",
version=(1, 0, 0),
run_on="device",
fb_type="facts",
param_cls=ConfigBackupParams,
result_cls=ConfigBackupResult,
is_pure=True,
is_idempotent=True,
)
)
Both flags are True because the block connects to a device, reads the running
configuration, and stores it — without modifying any device state.
Workflow failure semantics
How the engine classifies a workflow failure:
flowchart TD
A["Step N fails"] --> B{"All completed steps<br/>marked is_pure=True?"}
B -- Yes --> C["FAILED_SAFE"]
C --> D["Safe to retry<br/>entire workflow"]
B -- No --> E{"Failed step marked<br/>is_idempotent=True?"}
E -- Yes --> F["Retry failed step<br/>(up to 3 attempts)"]
F --> G{"Retry succeeded?"}
G -- Yes --> H["Continue workflow"]
G -- No --> I["FAILED_UNSAFE"]
E -- No --> I
I --> J["Manual intervention<br/>required"]
Implementation status
The idempotent auto-retry path (retry failed step up to 3 attempts) is a planned design
that is not yet enforced by the workflow engine. Currently, a failed step with a non-pure
predecessor always results in FAILED_UNSAFE. The is_idempotent flag is stored and
validated but does not yet trigger automatic retries.
See the Workflow Engine's retry and rollback documentation
for the current implementation status.
- All prior steps pure —
FAILED_SAFE. Retry the entire workflow. - Failed step is idempotent — retry that step (up to 3 attempts). (planned, not yet implemented)
- Neither —
FAILED_UNSAFE. Manual intervention required.
Best practices
Be conservative. Only mark is_pure=True if you are certain the block has
zero side effects — no external writes, no notifications, no file mutations.
Prefer idempotent designs. Use replace over append,
skip_if_identifier_exists when creating entities, and full config templates
rather than incremental patches.
Document non-obvious cases. If idempotency relies on subtle API behavior (e.g., PUT semantics), add a comment near the registration explaining why.
What about acquire() and side effects?
The acquire() phase runs before run() and is used to claim shared
resources (device locks, rate-limit tokens, etc.). The purity and
idempotency flags apply only to the run() method. Resource acquisition
and release are managed separately by the engine and are always safe to
retry.
See also:
- Types & Safety (Engine) -- How the engine uses
isPureandisIdempotentfor failure classification - Workflow as a Transaction -- The transaction model that these flags drive
Next: Async and Concurrency — learn how to run operations in parallel and offload blocking work to threads.