Behaviors
In Nebula, you don't build queues, endpoints, services, repositories, or dependency injection. You describe what composition of entities you care about, and the platform handles routing, execution, and state management. This is what makes a Nebula application fundamentally different from a traditional backend.
Declarative Interest
A system in Nebula declares a query that describes the entities it wants to process. When entities match that query — because they gained a component, lost one, changed a property, or had a relationship updated — Nebula routes them to the system automatically.
Here's a real production system that scores healthcare denial opportunities based on urgency and financial value:
The system doesn't poll. It doesn't subscribe to a message bus. It simply declares "I care about entities that have these four components" and Nebula ensures it sees every matching entity when changes occur.
Query Operators
Nebula's query language goes far beyond simple component existence checks. Operators compose together to express precise interest in entity state:
Real-time operators form the foundation — has, and, or, not, plus field comparisons like eq, gt, lte, contains, and oneOf.
Temporal operators let you reason about time:
asOf(date)— query entity state as it existed at a specific point in timechanged(start, end, properties)— find entities whose specific properties changed within a time windowduring(start, end)— find entities that maintained a particular state across a time period
Differential operators react to state transitions:
received/lost— entity just gained or lost a specific componentchildAdded/childRemoved— entity gained or lost a child relationshipparentAdded/parentRemoved— entity gained or lost a parent relationshipownerChanged— entity ownership was transferred
Relationship operators filter across the entity graph:
child(predicate)— match entities that have children matching a conditionparent(predicate)— match entities that have parents matching a condition
Ownership operators filter by principal:
mine()— entities owned by the current userownedBy(id)— entities owned by a specific principalowned(true/false)— entities that have (or lack) an owner
These operators compose naturally. A query that starts simple can become as specific as you need:
- Start broad —
has('Account')— all account entities - Add component requirements —
and(has('Account'), has('DenialOpportunity'))— accounts flagged for denial - Add temporal precision — add
changed('2024-01-01', '2024-03-01', ...)to find accounts whose claim status changed in Q1
Processing Results
When Nebula routes entities to your system, your code receives them in batches with full context. You can inspect the current and previous state of every component, make changes to the entities you received, and even find and modify related entities through programmatic search.
protected async processBatchCoreAsync(
batch: readonly EntityContext[]
) {
for (const ctx of batch) {
const opp = ctx.get<IDenialOpportunity>('DenialOpportunity');
const atf = ctx.get<IAppealTimelyFiling>('AppealTimelyFiling');
const daysRemaining = computeDaysRemaining(atf.deadline);
const urgencyScore = computeUrgencyScore(daysRemaining, atf.days);
const children = await this.getEntityChildrenAsync(ctx.id, {
limit: 500,
});
const totalValue = sumClaimValues(children.results);
ctx.upsert('DenialPriority', {
daysRemaining,
totalValue,
urgencyScore,
compositeScore: urgencyScore * totalValue,
calculatedAt: new Date().toISOString(),
});
}
// All changes are committed atomically at the end
}
All changes are committed atomically after your code returns. If something fails, nothing is partially written. This transactional guarantee means your system code doesn't need to handle rollbacks or partial state.
Frontend Queries
The same query language works in the browser. React components use useEntities to declare what data they need, and Nebula keeps the results up to date in real-time via WebSocket subscriptions.
import { and, has } from '@nebula/sdk';
import { useEntities, useSubscription } from '@nebula/react';
function DenialQueue() {
const query = and(has('Account'), has('DenialPriority'));
const { entities, total, hasMore } = useEntities(query, 0, 20);
useSubscription(query, {
onCreated: (entity) => { /* new match appeared */ },
onUpdated: (entity) => { /* existing match changed */ },
onDeleted: (entity) => { /* match no longer qualifies */ },
});
return (
<div>
<h2>{total} accounts in denial queue</h2>
{entities.map(entity => (
<AccountRow key={entity.id} entity={entity} />
))}
</div>
);
}
There's no separate API layer between your frontend and the data. The query you write in React is the same query your systems use on the backend.
Behaviors define what your application cares about. Orchestration explains how Nebula ensures those behaviors execute reliably at scale. For creative ways to apply these query patterns, see Component Query Refiners and Async Workflows.