iolite/Nebula

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.

INGESTIONEDI ClaimsEDI RemitsUCRNsACCOUNT CREATIONClaimAccountMatchingnot(parent(has('Account')))UcrnAccountCreationAccount+ CustomerPARALLEL ENRICHMENTDenialOpportunityIdentificationnot(has('DenialOpportunity'))LocationMatchingnot(has('LocationMatch'))TRIGGERED BY COMPONENTSTeamAssignmentreceived('DenialOpportunity')DenialPriorityhas('DenialOpportunity')Ready for Work QueueAccount + Customer + DenialOpportunity + DenialPriority+ Team link + LocationMatchNo central orchestrator.Workflow emerges fromcomponent queries.

Fan-Out: Parallel Ingestion

Three ingestion pipelines create entities independently:

ClaimAccountMatchingSystem

and
hasClaim
hasCustomer
notno Account parent
parent
hasAccount

UcrnAccountCreationSystem

and
receivedUCRN
hasCustomer

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:

DenialOpportunitySystem

and
hasAccount
hasCustomer
notDenialOpportunity
hasDenialOpportunity
child
hasClaim

LocationMatchingSystem

and
hasAccount
hasCustomer
notLocationMatch
hasLocationMatch
child
hasClaim

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:

TeamAssignmentSystem

and
hasAccount
hasCustomer
hasDenialOpportunity
notno Team parent
parent
hasTeam
hasStandalone

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:

DenialWorkQueue (frontend)

and
hasAccount
hasDenialPriority
hasDenialOpportunity
parent
hasTeam
hasStandalone

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:

  1. T+0s — EDI ingests a Claim. ClaimAccountMatching creates an Account, links Claim as child.
  2. T+1s — DenialOpportunitySystem detects denial codes, adds DenialOpportunity.
  3. T+1s — LocationMatchingSystem matches by tax ID, adds LocationMatch and links to Location.
  4. T+2s — TeamAssignmentSystem sees received('DenialOpportunity'), assigns to Team.
  5. T+2s — DenialPrioritySystem scores urgency, adds DenialPriority.
  6. 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.