The Determinism Rule
On this page
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.
- Presence of non-deterministic calls — Search for
datetime.now,uuid.uuid4,random.randomin the workflow code - Direct I/O — Any network or file call must live in an activity
- Environment variables — If you use
os.environfor branching, move the read into an activity - 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