---
title: "Reactive sessions and workflow events"
description: "Open-source agent-first creative workspace. Agents build images, video, storyboards, timelines, and games in real editors, and you keep an editable project."
canonical: https://docs.nodetool.ai/plans/reactive-sessions-events
markdown: https://docs.nodetool.ai/plans/reactive-sessions-events.md
product: NodeTool
source: https://github.com/nodetool-ai/nodetool/blob/main/docs/plans/reactive-sessions-events.md
---

# Reactive sessions and workflow events

# Reactive sessions and workflow events

## Recommendation and scope

Start with a small host adapter for invoking an existing operation and observing its result. Reuse Mini App operation dispatch, invocation identity, message folding, and cleanup. Two app hosts already repeat the same admission sequence: the web `useAppRuntime` hook and the headless `debug_app` runtime. Extract that shared sequence, prove that a non-app caller can use it, and only then consider a broader reactive-session module.

A reactive session names the caller's interaction lifetime. It is not a new workflow executor, job scheduler, state store, event broker, or background service. The initial delivery handles one declared operation and finite workflow results. Persistent streaming and durable triggers remain their existing execution paths, with separate adapters only when needed.

App agents and running games are initial consumers. Common code must not depend on conversation models or rendering clocks. The [app agents plan](/plans/realtime-virtual-agents.md) owns agent integration and conversation behavior. The [shared variables plan](/plans/shared-variables-input-bindings.md) owns new typed bindings and revision-checked publication. The [control-flow regions plan](/plans/control-flow-regions.md) owns branching and iteration within a workflow.

This review inspected the sources below and existing test cases. It did not execute them or measure realtime performance. Proposed contracts are distinguished from current implementation.

## What already exists

| Finding | Inspected behavior | Consequence |
| --- | --- | --- |
| F1. The web app host already coordinates reactive work | [useAppRuntime](https://github.com/nodetool-ai/nodetool/blob/main/web/src/components/appbuilder/runtime/useAppRuntime.ts) feeds an input binding into an owned active runner with `streamInput`. Its reactive path starts a full run first, then uses eligible browser subgraphs or falls back to a full run. During a reactive subgraph run it retains one latest pending trigger in `reactive.pending`. Its `dispatch(action, source?)` returns `void`, and the hook exports no other entry point | Extract or adapt the smallest reusable host coordination. Do not rebuild a reactive runner. The pending-trigger behavior is specific to this path, not a universal queue guarantee. A caller outside React cannot await a web-host operation today |
| F2. App run policy and input capture already exist | [decideRun](https://github.com/nodetool-ai/nodetool/blob/main/packages/app-runtime/src/policy.ts) returns `{ kind: "start" }`, `{ kind: "replace"; cancel }`, or `{ kind: "queue"; after }`. The web host resolves operation parameters with `resolveOperationParams`, reserves the invocation synchronously, then cancels or waits with `cancelInvocations`/`awaitSettled` | Reuse the pure decision and host behavior together. The decision function alone is not a scheduler. Preserve admitted input snapshots |
| F3. App result folding already correlates messages | [fold.ts](https://github.com/nodetool-ai/nodetool/blob/main/packages/app-runtime/src/fold.ts) `messageToEvents` resolves each message through `FoldContext.resolveInvocation(job_id)` and produces state events. [state.ts](https://github.com/nodetool-ai/nodetool/blob/main/packages/app-runtime/src/state.ts) `applyEvent`, `liveInvocations`, and `isLiveInvocation` apply invocation/output ownership rules | Reuse folding and the authoritative app state. Add consumer request/world revision checks around delivery where needed. Existing ownership does not prove a game target is still valid |
| F4. Streaming workflow execution is available | [ExecutionSession](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution/src/session.ts) exposes `pushInput`, `finishInputStream`, `result`, and `cancel`. Its `messages` getter works only when the session was created with `captureMessages: true`. [Runner input dispatch](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution-core/src/runner.ts) passes each value through the input node's executor, then puts it into the downstream inbox | A successful push means input processing/enqueue completed, not that downstream computation or a response completed. The session result ends the whole run. Per-request replies need explicit correlation in graph outputs |
| F5. Input buffering has one opt-in limit and one default limit | [NodeInbox](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution-core/src/inbox.ts) awaits capacity in `put` only when a per-handle `bufferLimit` is set, and that limit defaults to `null`. Its correlation pending-key caps default to 10,000 | Per-handle buffering is unbounded unless configured. Inspect actual configuration and await behavior before using a high-rate source. Keep this out of the first finite-operation adapter |
| F6. Durable triggers are an existing subsystem | [Boot wiring](https://github.com/nodetool-ai/nodetool/blob/main/packages/websocket/src/triggers/boot.ts) injects `DrizzleTriggerInputStore` and `DrizzleDurableInboxStore`. [Wakeup delivery](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution-core/src/trigger-wakeup.ts) stores/deduplicates inputs. [Dispatcher](https://github.com/nodetool-ai/nodetool/blob/main/packages/websocket/src/triggers/dispatcher.ts) starts headless jobs, separates acceptance from completion, and implements `parallel`/`queue`/`skip` | Keep durable input ingestion and dispatch here. The wakeup class's name does not establish general live-run resumption. Its default stores are in memory, while the normal enabled server boot supplies persistent stores. Accepted-run replay has a documented at-least-once crash window |
| F7. Invocation surfaces have different result contracts | [Workflow service](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution/src/service/workflow-run.ts) returns `RunWorkflowOutcome`. Its background path returns an inline receipt payload with `job_id` and `status: "running"`, and results come through existing job tools. [App-operation execution](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution/src/service/app-operation.ts) `executeAppOperation` reserves by invocation identity and returns `AppOperationOutcome`. A reused reservation returns `reused: true` with no messages and no terminal completion | Preserve each service's authority. Reservation/acceptance is not completion. Do not promise a common completion stream until its adapter is verified |
| F8. A non-React app host already exists | [HeadlessAppRuntime](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution/src/app-debug/runtime.ts) backs `debug_app` and `nodetool app debug`. Its `dispatch` is async and awaits a `run` action. It repeats the web host's admission order: resolve parameters, `decideRun`, cancel or wait, then `runStarted`. Its `runWorkflow(params)` callback takes no abort signal, `cancel` only marks state, a timeout leaves the invocation live, invocation IDs are local `headless-N` values, and messages are folded after the run returns | Two hosts already duplicate admission. The extraction target is the sequence both repeat, not a third host. This host proves app semantics without React, but it does not prove streamed folding, real cancellation, or server reservation identity. Its debug recording path reaches `executeAppOperation` through [run-recording.ts](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution/src/app-debug/run-recording.ts) |

The [inline graph runner](https://github.com/nodetool-ai/nodetool/blob/main/web/src/lib/workflow/runInlineGraphJob.ts) already routes eligible graphs to the browser and others to the existing WebSocket path, returning a result promise and supporting message observation. Use that through the existing app host where it already participates. Bypassing app reservation and accounting to call it directly would not be equivalent app execution.

The [app-runtime package](https://github.com/nodetool-ai/nodetool/blob/main/packages/app-runtime/README.md) is shared across hosts and intentionally dependency-free. Its pure state, policy, and fold functions stay there. Browser transport, React lifecycle, credentials, and server execution must not be imported into that package as part of extraction.

## Ownership and integration

| Responsibility | Authority |
| --- | --- |
| Current inputs, variables, and revisions | Existing app state or the caller's game/session state. The adapter samples that owner and does not keep another mutable copy |
| Which event requests work | Declared host binding or specialized behavior. Editing a value alone does not implicitly request execution |
| Invocation policy and authorized dispatch | Existing app/operation host, capability gate, or trigger dispatcher, according to the entry path |
| Graph computation, job status, usage, artifacts | Existing execution owner and run records |
| Request correlation and consumer lifetime | A small caller adapter mapping request identity to existing invocation/job identity and the consumer's reset generation |
| Whether a result is still relevant | Consumer policy, using target identity and source revision where needed |

Reuse the same mechanics for a background action that an app agent turn requests and for a game's semantic event. The app agent's turn is itself an app operation run. A game continues ticking and applies an eligible workflow result at its own update point. Neither calls model inference synchronously inside audio or rendering callbacks.

A sequential caller may await a workflow's terminal result before its next logical step. An interactive caller can observe that same completion asynchronously while continuing unrelated updates. This does not require separate execution modes or a second job.

Values, events, and media streams retain different semantics. Shared input-binding work is not a prerequisite for the first adapter. Use current resolved operation inputs and consumer-owned revision tokens. Audio and texture buffers remain in their dedicated paths, not in an app variable or a general event queue.

## Smallest first implementation

The first deliverable is one finite operation invoked from an existing Mini App host and from a minimal non-React caller fixture. Use a delayed deterministic workflow, not a paid provider or a new event source. Do not build a general session API first. Finding, action, and decision IDs are local to this plan and are independent of the app agents plan's IDs.

| Step | Concrete work | Acceptance |
| --- | --- | --- |
| A1. Pin existing host behavior | Extend the web [app host tests](https://github.com/nodetool-ai/nodetool/blob/main/web/src/components/appbuilder/runtime/__tests__/useAppRuntime.test.tsx) and [instance tests](https://github.com/nodetool-ai/nodetool/blob/main/web/src/components/appbuilder/runtime/__tests__/useAppRuntime.instances.test.tsx) and the [headless runtime tests](https://github.com/nodetool-ai/nodetool/blob/main/packages/execution/tests/app-debug-runtime.test.ts) with one declared finite operation, delayed result, and reset during execution. Record where the two hosts' admission sequences differ before extracting code | Both fixtures exercise input capture, `decideRun`, invocation identity, folding, and terminal status. The web fixture also covers server reservation. Existing queue/replace behavior remains covered. No provider call is needed |
| A2. Expose one caller adapter | Neither host offers a usable entry point as is: the web `dispatch` returns `void`, and `HeadlessAppRuntime` has no abort path or streamed folding. Extract the shared admission sequence into a helper with reservation, cancellation, waiting, and execution injected. Keep its pure parts in `app-runtime` and its effects in the hosts. Add an awaitable completion handle to the helper's result | An event submits the declared operation with a captured input snapshot and consumer generation. The caller receives its existing invocation identity and a terminal result or explicit observation-ended outcome. It may await completion or receive a callback |
| A3. Prove a second consumer | Drive the helper from an agent-free fixed-tick loop built on the [game simulation session](https://github.com/nodetool-ai/nodetool/blob/main/packages/game-runtime/README.md), calling `createGameSession(...).step(input)` once per tick. Use A1's Mini App fixture as the other consumer and share it with app agent work | The loop keeps stepping while work runs. A result carries the original request identity. Reset or target change prevents stale application. A result does not implicitly request another run |
| A4. Integrate without parallel ownership | Move the web host and `HeadlessAppRuntime` onto the extracted helper. Add consumer close/unsubscribe and explicit task-cancellation handling | There is one implementation of the admission sequence, one authoritative app/run state, and one counted operation. Failure, duplicate observation, reset, and late completion have defined outcomes. `debug_app` reports are unchanged for existing fixtures |

For the new non-React fixture, allow at most one outstanding request per caller and report busy explicitly for a second request. This limits the first integration, not the existing app's behavior or all agent tools globally. Do not add a queue or new replacement policy. Existing app operations keep their declared policies. A local consumer admission limit is not a replacement for server budgets or authorization.

The adapter needs only a declared operation reference, captured inputs, request identity, consumer generation, and optional target revision. A target revision tracks relevant input or object changes, not every render tick. Obtain the effective user/instance authority from the existing host, never from an untrusted event payload. Reuse invocation IDs when their scope is sufficient. A new consumer request ID is correlation, not a claim of durable deduplication.

Keep local admission, persisted reservation, and terminal execution distinct. An observer must attach to a reused active invocation's existing completion/status path instead of treating its reservation as a finished result. Do not automatically retry an effectful request after an uncertain acceptance.

Closing observation invalidates delivery, removes subscriptions, and settles any pending observer wait with an explicit observation-ended outcome. A wait timeout does the same unless the caller explicitly requests job cancellation. Neither outcome claims that execution stopped. Explicit cancellation calls the existing execution owner. Closing a caller does not request job cancellation (D2). Reset must invalidate old observers before new work is admitted. An error or late result can remain in existing run history without being applied to the new consumer.

Detaching or resetting does not free execution capacity for work still running. Preserve the host's in-flight accounting until settlement, including after a cancellation request. The initial caller limit must not be bypassed by repeatedly resetting its observer.

Use existing promises and messages for finite completion. Neither host exposes a safe completion handle today (F1, F8), so add a narrow handle at the extracted helper's interface. Do not introduce a global registry, new database table, generic transport, or replacement message fold merely to provide one. Settle the caller once, even if terminal status is observed through more than one existing path.

### Decisions for the first adapter

| Decision | Choice | Reason |
| --- | --- | --- |
| D1. Where the shared code lives | Pure admission steps in `app-runtime`, effectful steps injected by each host | `app-runtime` must stay dependency-free and shared by web, mobile, and `app debug`. The hosts keep transport, persistence, and credentials |
| D2. Whether closing a caller cancels its job | No. Close and timeout end observation only. Cancellation is a separate explicit call | Matches `HeadlessAppRuntime`, whose timeout leaves the invocation live. Implicit cancellation on close would turn a navigation or reset into a lost paid result |
| D3. How the headless host cancels | Add an optional abort signal to its `runWorkflow` callback before claiming cancellation in A4 | Today its `cancel` only marks state, so a cancellation fixture would pass without stopping work |
| D4. Second-consumer fixture | A fixed-tick loop over `game-runtime`, with no renderer and no live scene commands | The simulation is deterministic and already headless. A live scene command channel does not exist and belongs to the app agents plan's A10 |

## Subsequent adapters, only when needed

| Follow-up | Reuse | Additional proof before enabling |
| --- | --- | --- |
| A5. Persistent streaming input | Existing app `streamInput`, `ExecutionSession.pushInput`, stream completion, and captured messages | Confirm the graph/input supports repeated data and is ready. Serialize ingress where ordering is required. Distinguish input enqueue from response completion. Correlate repeated outputs, propagate ingress errors, and verify bounded buffering, end-of-stream, cancellation, and cleanup |
| A6. Durable trigger source | Existing registration, wakeup stores, dispatcher, scheduler, and headless jobs | Route completion to an authorized consumer only when needed. Test acceptance versus completion and redelivery without claiming exactly-once side effects. Do not insert a second dispatcher between stored events and execution |
| A7. Higher-rate interaction | Existing app reactive routing and source-appropriate policies | Define queue bounds or observation coalescing, stale-result handling, and overflow reporting. Preserve full-run fallback and effect restrictions. Prove that expensive work cannot starve the host loop |

A5–A7 do not block the first finite-operation adapter. They are independent integration needs, not a mandatory new runtime platform. The game fixture proves reuse without adding a game event editor or full agent-directed scene actions.

App operations use parallel/queue/replace, trigger registrations use parallel/queue/skip, and the web reactive-subgraph path has its own latest-pending behavior. Keep those meanings explicit. Do not normalize them into a single enum that silently changes cancellation, snapshots, or event loss. Latest-value coalescing is appropriate for replaceable observations, not arbitrary semantic commands.

Trigger scheduling and recovery polling are not render clocks. Same-process dispatcher notification can start accepted work promptly, but that is not a hard realtime guarantee. Streaming `pushInput` does not establish crash recovery, per-event acknowledgments, or a global total order. State each adapter's actual delivery and lifetime guarantees.

## Verification and plan coordination

Use existing execution, app, and game harnesses. Test unknown/foreign invocation messages, out-of-order completion, input changes after admission, reset, duplicate terminal events, workflow failure, observer disposal, and explicit cancellation. Assert that the fixture inspected a real invocation and that invalid ownership/revision cases fail. Measure timing on target hardware separately from deterministic correctness tests.

The app agents plan may proceed with existing agent and Mini App paths while this adapter is being proven. This plan's Mini App/game fixtures can likewise proceed without an app agent implementation. Share the finite-operation fixture and host integration. An app agent's message is the existing chat composer run. Its interrupt-response, transcript, and conversation state remain its own agent binding, not new verbs on the finite-operation adapter. General variables, regions, voice, and durable personal tasks are not prerequisites.

For code changes, follow [repository verification requirements](https://github.com/nodetool-ai/nodetool/blob/main/AGENTS.md#mandatory-post-change-verification), including dependency builds before affected tests and the harness gate. This documentation-only review requires link and text checks, not application test execution.
