Skip to content

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.

is_pure=True

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.

is_idempotent=True

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.

  1. All prior steps pureFAILED_SAFE. Retry the entire workflow.
  2. Failed step is idempotent — retry that step (up to 3 attempts). (planned, not yet implemented)
  3. NeitherFAILED_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:


Next: Async and Concurrency — learn how to run operations in parallel and offload blocking work to threads.