ExecutionFlow is a cross-language library that connects structured pub/sub (Topics) with scoped context propagation (Contexts). It is implemented idiomatically in eight languages while preserving the same conceptual contract across all of them.
Most application frameworks either give you global state (bad) or require you to thread a context object through every function call (tedious). ExecutionFlow offers a third path:
- Topic: A named publish/subscribe channel. Publish an event once; all interested parties receive it.
- Context: Scoped key-value storage that follows the execution flow — whether that's an async task, a goroutine's
context.Context, or a thread-local stack frame. - Binding: Declare that when a Topic fires, a Context value is automatically derived from the event payload and injected into the current execution scope. Every subscriber (and all code that runs within the scope) sees that value without needing it passed explicitly.
This pattern is ideal for cross-cutting concerns: request IDs, user identities, trace spans, feature flags — anything that describes what is executing right now rather than what to compute.
Topic<Msg> ──bind──> Context<T> (via transform: Msg → T)
│
publish(msg)
│
subscribers notified with enriched context
└── Context<T>.get() returns transform(msg) during the call
Topic
| Method | Description |
|---|---|
topic(name) / NewTopic[T](name) |
Get or create a Topic by name |
topic.subscribe(fn) |
Register a subscriber; returns a handle to unsubscribe |
topic.publish(msg) |
Fire all subscribers; panics/exceptions are isolated per-subscriber |
topic.withValue(msg) |
Populate bound Contexts, publish, return scope handle + enriched ctx |
topic.runWithValue(msg, fn) |
Sugar: withValue + call fn + close scope |
topic.bindContext(ctx, transform) |
Declare that publishing msg should set context to transform(msg) |
Context
| Method | Description |
|---|---|
new Context<T>(name) |
Create a named context store |
context.withValue(val) |
Push val onto the scope stack for the current execution |
context.runWithValue(val, fn) |
Sugar: withValue + call fn + pop |
context.clear() |
Push a "no value" sentinel — hides any outer value |
context.runClear(fn) |
Sugar: clear + call fn + pop |
context.get() |
Return the current value, or empty/nil if not set |
context.snapshot() |
Capture current state as a Snapshot object for re-entry later |
Snapshot
| Method | Description |
|---|---|
context.snapshot() |
Capture the current context state into a Snapshot |
snapshot.withValue() |
Re-enter the snapshotted scope; returns a scope handle |
snapshot.runWithValue(fn) |
Sugar: withValue + call fn + close scope |
- For each bound Context (in registration order):
context.withValue(transform(msg)) publish(msg)— subscribers fire inside the enriched scope- Returns control to caller — scope remains active until closed
# Run all tests
make test-all
# Per language
make test-typescript
make test-python
make test-go
make test-java
make test-csharp
make test-ruby
make test-rust
make test-swift| Language | Package | README |
|---|---|---|
| TypeScript / Node.js | packages/typescript |
README |
| Python | packages/python |
README |
| Go | packages/go |
README |
| Java | packages/java |
README |
| C# | packages/csharp |
README |
| Ruby | packages/ruby |
README |
| Rust | packages/rust |
README |
| Swift | packages/swift |
README |
ExecutionFlow uses each language's idiomatic async-context mechanism:
- TypeScript —
AsyncLocalStorageautomatically propagates acrossawaitandPromise.then() - Python —
contextvars.ContextVarautomatically copies intoasyncio.create_task()children - Go — uses explicit
context.Contextthreading (Go has no implicit ambient context); pass the enrichedcontext.Contextto downstream functions and goroutines - Java —
InheritableThreadLocalautomatically copies the context stack into newThreadinstances at creation time; useSnapshotfor thread-pool scenarios where threads are reused - C# —
AsyncLocal<T>automatically propagates intoasync/awaitcontinuations - Ruby —
Fiber.prependcopies the context hash into newFiberinstances at initialization; useSnapshotto transfer context into pre-existing fibers - Rust —
tokio::task_local!ensures context follows the task across thread migrations on multi-threaded executors; useSnapshot::run_with_valuefor explicit cross-task propagation viatokio::spawn - Swift — uses
@TaskLocal; automatically propagates to childTask { }closures
| Language | Minimum version |
|---|---|
| TypeScript / Node.js | Node.js 22+, TypeScript 5.2+ |
| Python | 3.11+ |
| Go | 1.21+ |
| Java | 21+ (JDK) |
| C# | .NET 8+ |
| Ruby | 3.2+ |
| Rust | 1.75+ (with tokio) |
| Swift | 5.9+ (structured concurrency) |