Nexora is a Java execution engine that turns a high-level goal into a set of steps and runs them. You tell it what you want to happen, and it figures out the order, runs independent steps in parallel, and gives you back a result.
The idea is that you shouldn't have to hard-code execution logic. You declare capabilities (things the system can do), define which steps map to which goal keywords, and Nexora handles the rest: planning, scheduling, retrying on failure, and tracing what happened.
Three things set it apart from every other workflow engine:
- Pluggable planner SPI - the planner itself is a plugin. Swap in an LLM, a constraint solver, or your own rule engine. The built-in keyword matcher is just the default.
- Reactive plan amendment - a step can reshape the remaining plan based on what it produced. Inject new steps, skip pending ones, or override inputs for downstream steps, all at runtime without touching the planner.
- Capability contracts with automatic fallback - capabilities declare their expected latency and error rate. The engine monitors live call metrics and silently reroutes to a fallback capability when the primary starts breaching its contract.
When you call engine.execute("process order payment", context):
- The planner matches the goal against registered step definitions and builds a DAG
- The scheduler walks the DAG and starts every step whose dependencies are already done; independent steps run in parallel on virtual threads
- Each step flows through an interceptor pipeline (tracing -> retry -> timeout) before hitting the actual capability
- If a step returns plan amendments, the scheduler applies them before any dependent step begins
- The contract monitor tracks every call outcome; if a capability breaches its declared SLA, traffic is rerouted to its fallback
- Events fire as steps start, complete, fail, or when the plan is amended; you can subscribe to any of them
A concrete example: four steps, two run in parallel.
validate_order --+
+--> charge_card --> send_receipt
fetch_inventory -+
validate_order and fetch_inventory start at the same time. charge_card waits for validate_order. send_receipt waits for both. Nexora works all of this out from the dependsOn declarations. You don't schedule anything manually.
At runtime, validate_order can inject a new audit_log step into that DAG without the planner being involved at all.
- Java 21
- Maven 3.9+
mvn install -DskipTestsThe CLI fat JAR ends up at nexora-cli/target/nexora.jar.
No config file needed. The demo command showcases all three differentiating features in a single run:
java -jar nexora-cli/target/nexora.jar demoNexora Feature Demo
===================
Features demonstrated:
1. Pluggable planner SPI - rule-based planner wired via CompositePlanner
2. Reactive plan amendment - validate_order injects audit_log at runtime
3. Capability contracts - charge_card declares p99 SLA + fallback
Initial DAG (from planner):
validate_order --+
+--> charge_card --> send_receipt
fetch_inventory -+
validate_order will amend the plan at runtime, injecting:
--> audit_log (runs after validate_order, before send_receipt)
Execution:
✓ validate_order 37ms
~ plan amended: ADD_STEP -> audit_log
~ plan amended: MODIFY_INPUT -> send_receipt
✓ fetch_inventory 54ms
✓ audit_log 16ms
✓ charge_card 86ms
✓ send_receipt 22ms
Result: COMPLETED
Steps: 5 executed
Contract health (charge_card):
samples=1 error-rate=0% p99=86ms
The plan started with 4 steps and finished with 5. validate_order injected audit_log mid-run. charge_card was monitored against its declared p99=200ms SLA throughout.
nexora [--config <file>] <command>
| Command | What it does |
|---|---|
nexora run -g "<goal>" |
Execute an intent and stream step results |
nexora plan -g "<goal>" |
Dry run: show the DAG without executing anything |
nexora caps |
List all registered capabilities |
nexora plugins |
List active plugins |
nexora observe |
Start UI/API/metrics server for live process observability |
nexora demo |
Run the built-in feature demo |
nexora dlq list |
List dead letter queue entries (default: PENDING) |
nexora dlq replay <id> |
Replay a dead-lettered execution |
nexora dlq resolve <id> |
Mark a dead letter as resolved |
Pass -c '{"key":"value"}' to run to inject context values that steps can reference.
By default Nexora looks for nexora.json in the working directory. Point to a different one with --config.
{
"steps": [
{
"id": "validate_order",
"capabilityId": "validate_order",
"matchesGoalContains": "order"
},
{
"id": "charge_card",
"capabilityId": "charge_card",
"matchesGoalContains": "payment"
}
],
"retry": {
"maxAttempts": 3,
"initialDelayMs": 200,
"multiplier": 2.0,
"maxDelayMs": 10000
}
}A step is included in the plan when its matchesGoalContains string appears in the goal. Dependencies between steps are declared in code using StepDefinition's dependsOn set.
NexoraEngine engine = NexoraEngine.builder()
.withPlugin(myPlugin)
.withStepDefinition(new StepDefinition(
"validate_order", "validate_order",
goal -> goal.contains("order")
))
.build();
engine.subscribe(StepCompletedEvent.class, e ->
System.out.printf("done: %s in %dms%n", e.stepId(), e.elapsed().toMillis()));
ExecutionResult result = engine
.execute("process order payment", Map.of("orderId", "ORD-99"))
.get();The planner that converts a goal string into a DAG is itself a plugin. Implement Planner and return it from plannerProviders() in your NexoraPlugin:
public class MySmartPlanner implements Planner {
@Override
public PlannerDescriptor descriptor() {
return new PlannerDescriptor("my-planner", "LLM-backed planner", 100);
}
@Override
public boolean canPlan(Intent intent, PlanningContext context) {
return intent.getGoal().length() > 20; // handle complex goals
}
@Override
public Plan plan(Intent intent, PlanningContext context) {
// use context.availableCapabilities() to see what's registered
// build and return a Plan
}
}The engine tries planners in descending priority order. The built-in rule-based planner always sits last as the fallback. Registering a planner with priority 100 means it runs first; if canPlan() returns false, the next one is tried.
You can also register a planner directly without a plugin:
NexoraEngine.builder()
.withPlanner(new MySmartPlanner())
.build();A capability can reshape the remaining plan by returning amendments alongside its result:
return CapabilityResult.success(
Map.of("valid", true),
List.of(
// inject a new step that runs after this one
new AddStepAmendment(new Step("audit_log", "audit_log",
Map.of("orderId", InputBinding.literal(orderId)),
null, Set.of("validate_order"), null, null)),
// override an input for a downstream step
new ModifyInputAmendment("send_receipt", "audited", true),
// cancel a step that is no longer needed
new SkipStepAmendment("legacy_check")
)
);Amendments are applied by the scheduler before any dependent step begins. A PlanAmendedEvent fires for each one so you can observe every mutation.
Note: Stateful circuit breaker options (
openDurationandprobeInterval) are currently Unreleased.
Capabilities declare their expected operational behaviour. The engine monitors every call and reroutes traffic when a capability breaches its contract:
new CapabilityDescriptor(
"charge_card", "Charges the customer card",
List.of(), List.of(), false, false,
CapabilityContract.builder()
.p99Latency(Duration.ofMillis(200))
.maxErrorRate(0.05)
.windowSize(20)
.openDuration(Duration.ofSeconds(30))
.probeInterval(Duration.ofSeconds(10))
.fallback("charge_card_fallback")
.build()
)If charge_card starts exceeding 200ms p99 or failing more than 5% of the time over the last 20 calls, the engine silently opens the circuit and routes new calls to charge_card_fallback. The circuit remains OPEN for 30 seconds before transitioning to HALF_OPEN, where it probes the primary capability every 10 seconds. When the primary recovers, the circuit closes and traffic returns automatically. The caller sees a normal result either way.
Query live health at any time:
NexoraEngine.HealthSnapshot health = NexoraEngine.HealthSnapshot.from(
engine.capabilityHealth("charge_card"));
// health.state(), health.sampleCount(), health.errorRate(), health.p99Latency()A plugin is a JAR that implements NexoraPlugin and declares itself in META-INF/services/com.nexora.spi.NexoraPlugin.
public class MyPlugin implements NexoraPlugin {
@Override
public PluginDescriptor descriptor() {
return new PluginDescriptor("my-plugin", "1.0.0", "Does stuff", List.of(), null);
}
@Override
public void initialize(PluginContext ctx) {}
@Override
public List<CapabilityProvider> capabilityProviders() {
return List.of(
new CapabilityProvider() {
public CapabilityDescriptor descriptor() {
return new CapabilityDescriptor(
"my_capability", "My Capability",
List.of(), List.of(), true, false
);
}
public Capability create(PluginContext ctx) {
return request -> CapabilityResult.success(Map.of("result", "ok"));
}
}
);
}
@Override
public void shutdown() {}
}Load a plugin JAR at runtime:
engine.loadPlugin(Path.of("my-plugin.jar"), "my-plugin");Or wire it directly without a JAR (useful in tests):
NexoraEngine.builder().withPlugin(new MyPlugin()).build();Subscribe to any event type:
engine.subscribe(StepStartedEvent.class, e -> log.info("started: {}", e.stepId()));
engine.subscribe(StepCompletedEvent.class, e -> log.info("done: {}", e.stepId()));
engine.subscribe(StepFailedEvent.class, e -> log.error("failed: {} {}", e.stepId(), e.failureMessage()));
engine.subscribe(PlanAmendedEvent.class, e -> log.info("plan mutated: {} -> {}", e.amendmentType(), e.targetStepId()));
engine.subscribe(PlanCompletedEvent.class, e -> metrics.record(e.elapsed()));Event handlers run on the engine's executor, not the caller's thread. A handler that throws does not affect execution or other handlers.
The default retry policy is no retry. Override it globally:
NexoraEngine.builder()
.withDefaultRetryPolicy(
ExponentialBackoffPolicy.builder()
.maxAttempts(3)
.initialDelay(Duration.ofMillis(200))
.multiplier(2.0)
.maxDelay(Duration.ofSeconds(10))
.build()
)
.build();Backoff includes ±25% jitter to avoid thundering herd on simultaneous failures.
Nexora allows you to set a plan-level wall-clock execution deadline. If the execution exceeds this duration, the entire plan is cancelled: running steps continue, but not-yet-started steps are suppressed, the terminal execution status is set to TIMED_OUT, and saga compensation is triggered for all successfully completed steps.
You can configure a global engine-wide default deadline using the builder:
NexoraEngine engine = NexoraEngine.builder()
.withDefaultPlanDeadline(Duration.ofSeconds(5))
.build();You can also override the deadline per execution request:
// Sets a 2-second deadline specifically for this execution
CompletableFuture<ExecutionResult> future = engine.execute(
"process order payment notification",
Map.of("orderId", "ORD-42"),
Duration.ofSeconds(2)
);When a plan times out:
- The execution status resolves to
TIMED_OUT. - A
PlanTimedOutEventis published. - Saga compensation runs for completed steps if saga is enabled.
Nexora allows you to register webhook URLs to be notified asynchronously when an execution reaches a terminal state (COMPLETED, FAILED, or TIMED_OUT). This is particularly useful when triggering executions remotely via the API and awaiting their outcome.
To use webhooks securely, configure an HMAC-SHA256 signature secret in your nexora.json or via the NEXORA_WEBHOOK_SECRET environment variable:
{
"webhookSecret": "your-secure-secret-here",
"steps": []
}When invoking an execution, supply the webhookUrl and optionally filter which terminal webhookEvents trigger a callback:
curl -X POST http://localhost:9464/api/execute \
-H "content-type: application/json" \
-d '{
"goal": "process order payment",
"webhookUrl": "https://your-api.com/webhooks/nexora",
"webhookEvents": ["COMPLETED", "FAILED", "TIMED_OUT"]
}'Nexora will dispatch a JSON payload to your endpoint with the execution outcome. It signs the payload using the configured secret and passes the signature in the nexora-signature HTTP header for validation. It also utilizes exponential backoff to retry deliveries up to 3 attempts. Delivery attempts are persisted in the nexora_webhook_deliveries database table and can be queried for auditability via the API.
When a execution fails after exhausting all retries, Nexora writes a record to the nexora_dead_letters table and fires an ExecutionDeadLetteredEvent on the event bus. This gives operators a structured audit trail of every permanently failed execution and a way to replay or resolve them without querying the database directly.
Each dead letter record carries:
| Field | Description |
|---|---|
id |
UUID of the dead letter record |
executionId |
The original failed execution |
goal |
The intent goal string |
context |
The intent context (JSON) |
failureCode |
Machine-readable failure code (e.g. STEP_FAILED) |
failureMessage |
Human-readable error detail |
failedAt |
When the failure occurred |
reviewState |
PENDING, RESOLVED, or REPLAYED |
Subscribe to the event for alerting:
engine.subscribe(ExecutionDeadLetteredEvent.class, e ->
alerting.notify("Execution dead-lettered: " + e.executionId() + " code=" + e.failureCode()));Inspect and remediate via CLI:
# list all pending dead letters
nexora dlq list
# replay a failed execution (creates a new execution with the same goal and context)
nexora dlq replay <dead-letter-id>
# mark as resolved when no replay is needed
nexora dlq resolve <dead-letter-id> --reason "Root cause fixed in deployment 1.2.3"Or via the observability REST API:
# list PENDING (default)
curl http://localhost:9464/api/dead-letters
# filter by state, paginate
curl "http://localhost:9464/api/dead-letters?state=RESOLVED&page=0&size=10"
# replay
curl -X POST http://localhost:9464/api/dead-letters/<id>/replay
# resolve
curl -X POST http://localhost:9464/api/dead-letters/<id>/resolve \
-H "content-type: application/json" \
-d '{"reason":"investigated and closed"}'Authentication: DLQ endpoints will require a Bearer token once #30 lands.
Start Nexora's built-in observability server:
java -jar nexora-cli/target/nexora.jar observe --port 9464This exposes four endpoints with no external dependencies:
| Endpoint | What it serves |
|---|---|
GET / |
Live process UI showing active executions, step timelines, and plan amendments |
GET /metrics |
Prometheus text format scrape endpoint |
GET /api/process |
Raw process snapshot as JSON |
POST /api/execute |
Trigger an execution remotely |
GET /health/ready |
Check health of all capabilities (returns 503 if any circuit is OPEN/HALF_OPEN) |
GET /api/webhook-deliveries/{id} |
Audit log of webhook delivery attempts for an execution |
GET /api/dead-letters |
List dead letter queue entries (paginated, filterable by ?state=PENDING|RESOLVED|REPLAYED|ALL) |
POST /api/dead-letters/{id}/replay |
Create a new execution from a dead letter and mark it as REPLAYED |
POST /api/dead-letters/{id}/resolve |
Mark a dead letter as RESOLVED with an optional {"reason":"..."} body |
Note: The
/health/readyendpoint is currently Unreleased.
Example execute request:
curl -X POST http://localhost:9464/api/execute \
-H "content-type: application/json" \
-d '{"goal":"process order payment notification","context":{"orderId":"ORD-99"}}'Metrics exposed include:
nexora_plan_started_total,nexora_plan_completed_total,nexora_plan_failed_totalnexora_plan_duration_seconds— histogram by status (completed/failed)nexora_step_started_total,nexora_step_completed_total,nexora_step_failed_total— all by capability IDnexora_step_duration_seconds— histogram by capability ID and terminal statusnexora_plan_amendments_total— by amendment type (ADD_STEP, SKIP_STEP, MODIFY_INPUT)nexora_active_executions— current in-flight count
Bring up Prometheus, Grafana, and Alertmanager with the prebuilt stack:
cd observability
docker compose up -d- Prometheus:
http://localhost:9090 - Alertmanager:
http://localhost:9093 - Grafana:
http://localhost:3000(login:admin/admin)
The Grafana dashboard and alert rules are provisioned automatically. Provisioned assets:
observability/prometheus/prometheus.ymlobservability/prometheus/alerts.ymlobservability/grafana/dashboards/nexora-overview.json
You can also attach observability to an engine in your own application without the HTTP server:
NexoraEngine engine = NexoraEngine.builder()...build();
try (NexoraObservability obs = NexoraObservability.attach(engine)) {
engine.execute("process order", context).get();
String metrics = obs.scrapePrometheus(); // Prometheus text format
ProcessSnapshot snapshot = obs.processSnapshot(); // live execution state
}nexora-core Domain types: Intent, Plan, Step, PlanAmendment, ExecutionContext
nexora-plugin-spi Plugin contract: NexoraPlugin, Capability, Planner, CapabilityRegistry
nexora-registry DefaultCapabilityRegistry (thread-safe, read-write locked)
nexora-tracing Tracer/Span interfaces + no-op implementation
nexora-retry RetryPolicy, ExponentialBackoffPolicy with jitter
nexora-event ExecutionEvent sealed hierarchy, InProcessEventBus
nexora-executor DAG scheduler with amendment support, interceptor pipeline, contract monitor
nexora-plugin-loader PluginClassLoader, PluginManager, lifecycle FSM
nexora-planner CompositePlanner, RulePlanner, StepDefinition, PlanRegistry
nexora-runtime ExecutionEngine (ties planner + scheduler together)
nexora-api NexoraEngine public facade, builder, NexoraObservability
nexora-cli PicoCLI command-line interface + ObserveCommand HTTP server
Dependencies always point inward. nexora-cli knows about nexora-api. nexora-api knows about nexora-runtime. Neither knows anything about the CLI.