Workflow Context Management#
DBOS uses a single module-level ContextVar to store per-execution workflow state in a thread-safe and async-safe way. All context reads and writes go through accessor functions wrapping this variable; no code accesses the ContextVar directly. Context managers at every execution boundary (workflow, step, transaction, handler) install a fresh DBOSContext object on entry and restore the previous one on exit, creating an implicit stack of execution scopes.
_dbos_context_var — the root ContextVar#
_dbos_context_var is the single ContextVar[Optional[DBOSContext]] that holds the context for the currently executing operation. Three thin wrappers are the only intended interface:
_set_local_dbos_context(ctx)— install a context_clear_local_dbos_context()— set toNoneget_local_dbos_context()— read the current context
assert_current_dbos_context() is a convenience assertion used inside decorators where a context is always expected.
DBOSContext — the state object#
DBOSContext is a plain dataclass-like object (not itself a ContextVar). Each instance captures everything needed to execute one DBOS operation:
| Field | Purpose |
|---|---|
workflow_id | Identifies the running workflow (empty = outside a workflow) |
function_id | Monotonically-incremented step counter for deterministic replay |
parent_workflow_id / parent_workflow_fid | Link to enclosing workflow for child workflows |
sql_session | Active SQLAlchemy session (non-None only inside a transaction) |
curr_step_function_id | Step in progress (-1 = none) |
workflow_deadline_epoch_ms / workflow_timeout_ms | Propagated deadlines |
authenticated_user / authenticated_roles / assumed_role | Auth context propagated into child operations |
deduplication_id, priority, queue_partition_key, etc. | Queue enqueue options |
State predicates — is_workflow(), is_transaction(), is_step() — determine which type of operation is active.
Context propagation: explicit copy, not copy_context()#
Python's contextvars.copy_context() is not used. Instead, DBOS propagates context by constructing DBOSContext child objects explicitly:
DBOSContext.create_child()creates a new context inheritingparent_workflow_id, auth fields, and theis_within_set_workflow_id_blockflag from the parent.DBOSContext.create_start_workflow_child()is the entry point for spawning child workflows; it increments the parent'sfunction_idand assigns a deterministic child ID ({parent_id}-{function_id}).snapshot_step_ctx()creates an immutable snapshot of the current workflow context for a step call, atomically incrementingfunction_idon the parent so two concurrent steps never share an ID.- The top-level helper
snapshot_step_context()returns this snapshot when called from inside a workflow, or the existing context otherwise.
The resulting DBOSContext object is passed as an explicit parameter to the worker function (new_wf_ctx at lines 930–938), where EnterDBOSWorkflow installs it into _dbos_context_var via _set_local_dbos_context().
Context manager hierarchy#
Each execution boundary is wrapped by a context manager that saves/restores _dbos_context_var:
| Context manager | Used for | Key behavior |
|---|---|---|
EnterDBOSWorkflow | Workflow entry (sync & async) | Saves previous context, installs new one, calls ctx.start_workflow() / ctx.end_workflow(), restores on exit |
EnterDBOSStepCtx | Step execution | Swaps in the step snapshot context, calls start_step() / end_step() |
EnterDBOSTransaction | Transaction execution | Increments function_id, calls start_transaction() / end_transaction() — does not swap context objects |
EnterDBOSHandler | HTTP handler entry | Creates a minimal context if none exists, tears it down on exit |
DBOSContextEnsure | Guarantees a context is present | Creates a bare DBOSContext() if none installed |
In practice, the sync workflow wrapper chains these via the Outcome.also() / Outcome.then() combinator API . The thread-pool worker _execute_workflow_wthread and the async counterpart _execute_workflow_async both enter EnterDBOSWorkflow as their outermost with block, so each thread/coroutine gets its own isolated _dbos_context_var binding.
For steps called within a workflow, run_step() calls ctx.snapshot_step_ctx() on the live workflow context and passes the snapshot to invoke_step(), which wraps it in EnterDBOSStepCtx .
User-facing context manipulation#
Several public context managers let callers inject configuration before invoking a workflow or queue operation; each saves and restores the relevant fields:
SetWorkflowID— pin a deterministic ID for the next workflowSetWorkflowTimeout— set a timeout/deadline (clears any propagated deadline)SetEnqueueOptions— set deduplication ID, priority, partition key, or delayDBOSContextSetAuth— inject authenticated user/roles for testing or middlewareDBOSAssumeRole— temporarily assume a role
All of these call get_local_dbos_context() at __enter__ time and will create a bare context if one doesn't exist yet.
Key files#
dbos/_context.py— entire context system:DBOSContext, theContextVar, all context managersdbos/_core.py— workflow/step/transaction decorators; calls all of the above context managers