Skip to main content
Lesson 8 of 14 · Mistral Workflows: Durable Orchestration

The Determinism Rule

Lesson 8 of 14 Updated November 2, 2026 Advanced
On this page
  1. Why Determinism Is Mandatory
  2. What This Looks Like in Code
  3. SDK Replacements
  4. The Boundary to Remember
  5. Diagnosing a Non-Determinism Error
  6. Key Takeaways

Why Determinism Is Mandatory

Determinism is the most critical constraint in Mistral workflows, and it stems directly from the replay mechanism you learned about earlier. When a worker resumes an interrupted workflow, it replays the workflow code from the beginning and compares, operation by operation, what the code produces against what the history log recorded. As long as the two sequences match, the resumption is invisible. The moment they diverge, the platform has no way to know where it left off: it detects the inconsistency and fails the workflow with a non-determinism error.

In practice, a deterministic workflow produces the exact same sequence of operations every time it runs with the same inputs. Note the nuance: activity results can vary between executions since they come from the journal. It is the order of calls that must remain identical.

What This Looks Like in Code

Consider a workflow that sends a report to a user based on their subscription status.

@workflows.workflow.define(name="deterministic_workflow")
class DeterministicWorkflow:
    @workflows.workflow.entrypoint
    async def run(self, user_id: str) -> dict:
        # Always the same call sequence for the same input
        profile = await get_profile(user_id)
        
        if profile["premium"]:
            report = await generate_full_report(user_id)
        else:
            report = await generate_basic_report(user_id)
        
        await send_email(profile["email"], report)
        return {"status": "completed"}

This workflow is deterministic even though it contains a branch. The reason lies in the source of the condition: profile["premium"] comes from an activity result, so it comes from the journal. During replay, get_profile returns the exact same value as the first execution, and the same path is taken.

Now compare that with this version, which looks harmless but breaks everything.

import datetime
import random

@workflows.workflow.define(name="non_deterministic_workflow")
class NonDeterministicWorkflow:
    @workflows.workflow.entrypoint
    async def run(self, user_id: str) -> dict:
        # FORBIDDEN: datetime.now() changes on every execution
        if datetime.datetime.now().hour < 12:
            await send_morning_email(user_id)
        else:
            await send_afternoon_email(user_id)
        
        # FORBIDDEN: random.random() yields a different result every time
        if random.random() > 0.5:
            await send_notification(user_id)
        
        return {"status": "completed"}

Imagine the worker crashes at 11:58 AM and restarts at 12:03 PM. On replay, datetime.now() returns an afternoon time while the history contains a call to send_morning_email. The workflow fails. random.random() produces the same effect, only harder to diagnose.

SDK Replacements

The SDK doesn’t just forbid these calls; it provides deterministic equivalents for the three most common operations.

from mistralai.workflows import workflow

# Get current time (deterministic during replay)
current_time = workflow.now()          # Instead of datetime.now()

# Generate a unique ID (deterministic during replay)
request_id = workflow.uuid4()          # Instead of uuid.uuid4()

# Get a random number (deterministic during replay)
rand_value = workflow.random()         # Instead of random.random()

The principle is the same as for activities: the value is calculated and recorded in the journal on the first execution, then simply replayed afterward. The previous example then becomes perfectly safe.

import mistralai.workflows as workflows
from mistralai.workflows import workflow

@workflows.workflow.define(name="fixed_workflow")
class FixedWorkflow:
    @workflows.workflow.entrypoint
    async def run(self, user_id: str) -> dict:
        # CORRECT: workflow.now() is deterministic during replay
        current_hour = workflow.now()
        if current_hour.hour < 12:
            await send_morning_email(user_id)
        else:
            await send_afternoon_email(user_id)
        
        # CORRECT: workflow.random() is deterministic during replay
        if workflow.random() > 0.5:
            await send_notification(user_id)
        
        # CORRECT: workflow.uuid4() is deterministic during replay
        trace_id = workflow.uuid4()
        await log_trace(str(trace_id), user_id)
        
        return {"status": "completed", "trace_id": str(trace_id)}

The Boundary to Remember

The following are forbidden in workflow code: datetime.now(), datetime.utcnow(), and time.time(); uuid.uuid4() and uuid.uuid1(); random.random(), random.randint(), and random.choice(); any direct network call (httpx, aiohttp, requests); any file read or write (open(), aiofiles); any database access; os.environ, because environment variables can change between replays; and finally threading and multiprocessing, since the platform handles internal parallelism.

Everything reproducible remains allowed: calling activities via await, using workflow.now(), workflow.uuid4(), and workflow.random(), writing conditional logic or loops whose conditions depend on activity results, manipulating in-memory data (calculations, strings), and logging—logging is considered an acceptable side effect.

Diagnosing a Non-Determinism Error

When the error strikes, four checks usually suffice to locate it.

  1. Presence of non-deterministic calls — Search for datetime.now, uuid.uuid4, random.random in the workflow code
  2. Direct I/O — Any network or file call must live in an activity
  3. Environment variables — If you use os.environ for branching, move the read into an activity
  4. Third-party libraries — Some libraries make network calls or use randomness internally

This last point is the sneakiest: a serialization library that sorts by a randomly generated ID, or an HTTP client instantiated at module load, can fail a replay without a single suspicious line appearing in your workflow.

The most effective defense is to keep the workflow as lean as possible and push all complex logic into activities. A workflow that simply chains three calls has mechanically no opportunity to be non-deterministic.

# BEST PRACTICE: simple workflow, logic in activities
@workflows.workflow.define(name="simple_pipeline")
class SimplePipeline:
    @workflows.workflow.entrypoint
    async def run(self, params: dict) -> dict:
        step1 = await prepare_data(params)
        step2 = await process_data(step1)
        step3 = await finalize(step2)
        return step3

Key Takeaways

  • Workflow code must be deterministic: same inputs = same operation sequence
  • Use workflow.now(), workflow.uuid4(), workflow.random() instead of standard Python equivalents
  • All I/O operations (network, file, DB) must live in an activity, never in the workflow
  • Conditional branches are allowed if they depend on activity results
  • A determinism violation causes the workflow to fail during replay
  • When in doubt, move the logic into an activity