Branching Async Workflows
Nebula has no workflow engine, no step functions, no orchestrator. Yet complex multi-stage pipelines emerge naturally from independent systems reacting to entity state. Each system runs on its own, checks its own preconditions, and writes its own outputs. The "workflow" is the entity's component composition evolving over time — and no single system knows or controls the full picture.
The Scenario
A healthcare denial management platform ingests EDI data from three independent sources: Claims, Remits, and UCRNs. Each source arrives on its own schedule. The system must create Account entities, detect denial opportunities, match accounts to facilities, assign work teams, score priority, and surface fully-ready accounts to a work queue. All of this must happen without blocking on any single source and without a central coordinator.
The Pattern
Design each processing stage as an independent system with a query that describes its preconditions. Systems fan out from ingestion, converge on shared entities, and gate on component presence. The workflow advances when systems add components or links that satisfy downstream queries.
Fan-Out: Parallel Ingestion
Three ingestion pipelines create entities independently:
Both systems feed into the same Account entity through deduplication — if an Account already exists for a given account number, the Claim is linked to it rather than creating a duplicate.
Convergence: Independent Enrichment
Once an Account exists, multiple systems enrich it in parallel. None wait for each other:
Gating: Component-Triggered Stages
Some systems only run after a specific upstream system has finished. They gate on the output component of that upstream system:
Inside the system, a runtime guard prevents re-entrancy even if the query matches again:
// Guard: check if already linked to a team before assigning
const teamParents = await this.findEntitiesAsync(
and(has('Team'), child(id(ctx.id))), 10
);
if (teamParents.length > 0) continue; // Already assigned
const teamId = await this.selectTeam(ctx);
if (teamId) ctx.linkTo(teamId);
The "Fully Ready" State
No system declares "this account is ready." Instead, the work queue UI queries for the full component composition:
An account appears in the work queue only when all independent systems have completed their work. There's no "done" flag — the composition of components and links is the completion state.
In Practice
Here's the full timeline for a single Account:
- T+0s — EDI ingests a Claim. ClaimAccountMatching creates an Account, links Claim as child.
- T+1s — DenialOpportunitySystem detects denial codes, adds
DenialOpportunity. - T+1s — LocationMatchingSystem matches by tax ID, adds
LocationMatchand links to Location. - T+2s — TeamAssignmentSystem sees
received('DenialOpportunity'), assigns to Team. - T+2s — DenialPrioritySystem scores urgency, adds
DenialPriority. - T+2s — Account appears in the work queue. No system "published" it there — it just matched the query.
Steps 2-3 run in parallel. Steps 4-5 run in parallel but after step 2. If Remit data arrives late (T+30min), DenialOpportunitySystem re-evaluates and may update the codes — which re-triggers TeamAssignment and PriorityScoring through received('DenialOpportunity').
Why This Works
Building this workflow traditionally would require a message broker, a saga/orchestrator pattern, compensating transactions, and careful ordering logic. Each new stage means new queue subscriptions, new routing rules, and new failure modes.
In Nebula, each system is a pure function of entity state: "if the entity looks like X, do Y." There's no coordination overhead, no message ordering concerns, and no single point of failure. Adding a new stage means adding a new system with its own query — existing systems don't change. The workflow topology is an emergent property of component queries, not a declared configuration.
Behaviors explains the query-driven execution model. Orchestration covers how Nebula guarantees reliable execution. Relationship Patterns and Component Query Refiners detail the building blocks used in this pattern.