Skip to content

Shared Memory ​

SharedMemory is a namespaced key-value store that lives for the duration of a workflow run. Steps write to their own namespace, read from any namespace. It is the easiest way to share state across steps that do not have a dependsOn edge between them.

When to use it ​

The main data-passing channel between steps is dependsOn plus per-step content and result. That covers most cases: step A produces output, step B reads steps.A.content or steps.A.result because B depends on A.

Use shared memory when none of those apply:

  • Cross-branch state. Two parallel branches of the DAG (no dependsOn between them) need to coordinate. For example, a security audit branch wants to flag findings that an unrelated optimization branch should avoid.
  • Cumulative facts. A step that runs many times (forEach, repeat-until) wants to accumulate findings into a single store, and a downstream step reads the whole store.
  • Coordinator-curated context. The coordinator wants to record a fact that any future step can look up, regardless of dependsOn.

If two steps have a clean producer / consumer relationship, prefer dependsOn and step outputs. They are typed (result is map[string]any), validated (via resultSchema), and visible to CEL. Shared memory is unstructured - string-only values, agent-namespaced keys, no schema.

API ​

go
sm := zenflow.NewSharedMemory()

orch := zenflow.New(
    zenflow.WithModel(llm),
    zenflow.WithSharedMemory(sm),
    // ...
)

result, err := orch.RunFlow(ctx, wf)

// After the run, inspect the entries:
for k, v := range sm.Entries() {
    fmt.Printf("%s = %s\n", k, v)
}

SharedMemory exposes:

  • Write(agent, key, value string) - store value at "agent/key". Replaces any prior value.
  • Read(qualifiedKey string) (string, bool) - read the fully-qualified key ("agentName/key").
  • ListByAgent(agent string) map[string]string - return entries for one agent, with the namespace prefix stripped.
  • Entries() map[string]string - shallow copy of the full map (for persistence).
  • LoadEntries(map[string]string) - bulk load (used during resume).
  • Summary() string - markdown digest of all entries (- agent/key: value, value-truncated to 100 chars). Used as context-injection text.

The agent name is sanitised on Write: any / characters are replaced with _ to prevent namespace collisions. Two agents named team/a and team_a would collide; the sanitiser ensures only team_a exists.

Tool exposure ​

When WithSharedMemory is set, the executor auto-injects two tools into every step agent's tool catalogue:

  • shared_memory_write({key, value}) - writes under the calling agent's namespace.
  • shared_memory_read({key}) - reads from any namespace; the tool accepts a fully-qualified key.

Step agents call these like any other tool. The agent name is determined automatically from the step's agent assignment, so shared_memory_write({key: "found", value: "x"}) from agent auditor writes to auditor/found.

The two tools are not exposed to the coordinator by default - coordinator's role is routing, not state. If you want the coordinator to mutate shared memory, append the tools manually via WithCoordTools.

Lifecycle ​

A SharedMemory instance is per-run by default: pass a fresh NewSharedMemory() to the orchestrator, and entries from one RunFlow call do not leak into the next.

To share state across runs (e.g. a long-lived advisor that accumulates findings over many workflow invocations), keep the SharedMemory instance and re-use the orchestrator. Just remember the instance is in-process - restarting the binary loses the state.

Persistence ​

For resumable runs, the storage backend persists the shared memory entries on each step completion. On ResumeFlow, the executor calls LoadEntries to restore them before re-executing failed / cancelled / skipped steps.

This works automatically with the default MemoryStorage (intra-process) and any custom Storage implementation. The default is process-local; for cross-process resume, plug a persistent storage backend.

Worked example: cross-branch coordination ​

yaml
agents:
  auditor:
    description: "Security auditor."
  optimizer:
    description: "Performance optimizer."
  reporter:
    description: "Final report writer."

steps:
  - id: scan
    agent: auditor
    instructions: |
      Audit the auth module. For each issue, call:
        shared_memory_write({key: "issue-<n>", value: "<description>"})

  - id: profile
    agent: optimizer
    instructions: |
      Profile the auth module. Before suggesting optimizations, call
        shared_memory_read({key: "auditor/issue-1"})
      to check whether the auditor found anything that overlaps.

  - id: report
    agent: reporter
    dependsOn: [scan, profile]
    instructions: |
      Read every entry under auditor/ and optimizer/ and write a
      combined report.

scan and profile run in parallel (no dependsOn). They coordinate via shared memory: the auditor writes findings, the optimizer reads them. report depends on both and synthesises.

What it is not ​

  • A scratch buffer for the same step. Use Go-level state (or skip it entirely) within a single step.
  • A typed store. Values are strings. If you need structured data, JSON-encode it before writing and decode after reading. Or use step.result (which is structured) and dependsOn.
  • An IPC mechanism. Like the rest of zenflow, shared memory is in-process only.

Released under the Apache 2.0 License.