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.
Java code is formatted with palantir-java-format through Spotless. mvn verify (and CI) fails on unformatted code; fix it with:
mvn spotless:applyIn IntelliJ, install the palantir-java-format plugin and enable it in Settings > palantir-java-format, so Reformat Code and format-on-save produce the same result.
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 |
nexora schedule add |
Register a recurring cron-based execution |
nexora schedule list |
List schedules and next fire times |
nexora schedule remove <id> |
Cancel a schedule immediately |
nexora cancel <executionId> |
Cancel an execution running in the observe server |
nexora replay <executionId> |
Replay a failed, timed-out, cancelled or compensated execution through the observe server |
nexora user add <username> |
Create a dashboard user (also passwd, remove, list) |
nexora login |
Sign in with a username and password; saves a user API key for cancel / replay |
nexora logout |
Revoke and delete the saved user API key |
nexora tls-gen |
Write a development CA and server certificate for HTTPS on localhost |
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.
{
"executionStore": "./nexora-data",
"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
},
"persistence": {
"pool": {
"maximumPoolSize": 10,
"minimumIdle": 2,
"connectionTimeoutMs": 30000,
"idleTimeoutMs": 600000
},
"stepOutputMaxBytes": 65536
},
"observe": {
"rateLimit": {
"enabled": true,
"globalRpm": 60,
"executeRpm": 10,
"trustedProxies": []
}
}
}persistence.pool sizes the HikariCP connection pool behind executionStore. Every key is optional and the values above are the defaults (minimumIdle defaults to 2, or to maximumPoolSize if that is smaller). connectionTimeoutMs must be at least 250 and idleTimeoutMs must be 0 (never retire idle connections) or at least 10000; out-of-range values fail at startup.
persistence.stepOutputMaxBytes caps the output stored for each completed step, in UTF-8 JSON bytes (default 65536). A larger output is stored as a truncated preview and logged at WARN; the step still completes, and later steps still receive the whole output. Values from 1 to 255 fail at startup. Set it to 0 to store no outputs, for example when steps return data that must not be written to the database.
observe.rateLimit limits each client of nexora observe to globalRpm API requests a minute, executeRpm of them to POST /api/execute; more get 429 with Retry-After. Set enabled to false when a reverse proxy rate limits instead, and list proxies in trustedProxies so clients behind them are told apart by X-Forwarded-For. See Rate limiting.
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) were added in v0.2.0.
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 -H "Authorization: Bearer $NEXORA_API_KEY" -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 -H "Authorization: Bearer $NEXORA_API_KEY" http://localhost:9464/api/dead-letters
# filter by state, paginate
curl -H "Authorization: Bearer $NEXORA_API_KEY" "http://localhost:9464/api/dead-letters?state=RESOLVED&page=0&size=10"
# replay
curl -H "Authorization: Bearer $NEXORA_API_KEY" -X POST http://localhost:9464/api/dead-letters/<id>/replay
# resolve
curl -H "Authorization: Bearer $NEXORA_API_KEY" -X POST http://localhost:9464/api/dead-letters/<id>/resolve \
-H "content-type: application/json" \
-d '{"reason":"investigated and closed"}'Authentication: these endpoints require the API key. See Authentication.
Register recurring executions driven by standard 5-field UNIX cron expressions. Schedules are persisted to a nexora_schedules table and survive engine restarts. Requires executionStore to be configured.
NexoraEngine engine = NexoraEngine.builder()
.withExecutionStore(JdbcExecutionStore.h2("./nexora-data"))
.withPlugin(myPlugin)
.build();
// Register a nightly job
ScheduledExecution handle = engine.schedule(
"0 0 * * *",
new Intent("nightly cleanup", Map.of())
);
System.out.println("Next fire: " + handle.nextFireTime());
// Cancel at any time
handle.cancel();
// List active schedules
engine.listActiveSchedules().forEach(s ->
System.out.println(s.id() + " " + s.cronExpression() + " next=" + s.nextFireAt()));Three missed-fire policies control what happens when the engine restarts after downtime:
| Policy | Behaviour |
|---|---|
FIRE_ONCE |
Fire once immediately to catch up, regardless of how many windows were missed (default) |
SKIP |
Drop all missed windows and wait for the next scheduled slot |
FIRE_ALL |
Fire once for each missed window |
engine.schedule("*/5 * * * *", intent, MissedFirePolicy.SKIP);Subscribe to scheduling events:
engine.subscribe(ScheduledExecutionFiredEvent.class, e ->
log.info("fired scheduleId={} executionId={}", e.scheduleId(), e.executionId()));
engine.subscribe(ScheduledExecutionMissedEvent.class, e ->
alerting.warn("missed {} windows for schedule {}", e.missedCount(), e.scheduleId()));Manage via CLI:
nexora schedule add --goal "nightly cleanup" --cron "0 0 * * *"
nexora schedule add --goal "poll api" --cron "*/5 * * * *" --policy SKIP
nexora schedule list --active-only
nexora schedule remove <schedule-id>Or via the observability REST API (also exposed in the dashboard Cron Schedules panel):
curl -H "Authorization: Bearer $NEXORA_API_KEY" http://localhost:9464/api/schedules
curl -H "Authorization: Bearer $NEXORA_API_KEY" -X POST http://localhost:9464/api/schedules \
-H "content-type: application/json" \
-d '{"cronExpression":"0 0 * * *","goal":"nightly cleanup","missedFirePolicy":"FIRE_ONCE"}'
curl -H "Authorization: Bearer $NEXORA_API_KEY" -X DELETE http://localhost:9464/api/schedules/<id>Every execution belongs to a tenant, default unless the intent names one. History, dead letters, schedules, replays and cancels are scoped by it, capabilities read it from their request, and execution metrics carry a tenant label.
engine.execute(new Intent("process order", Map.of("orderId", "123")).withTenantId("acme"));
// An engine that only ever runs and sees acme's work
NexoraEngine acme = NexoraEngine.builder()
.withExecutionStore(new JdbcExecutionStore(sharedPool))
.withTenantId("acme")
.build();nexora observe --multi-tenant scopes the API and dashboard: users are bound to a tenant (nexora user add alice --tenant acme), and the server key names one with X-Tenant-ID. Try the library side with java -jar tests/multi-tenant-demo/target/multi-tenant-demo.jar and the server side with the login demo. See the Multi-tenancy guide.
Start Nexora's built-in observability server:
java -jar nexora-cli/target/nexora.jar observe --port 9464The server's full HTTP API is described by an OpenAPI 3.1 document at GET /openapi.yaml, browsable with Swagger UI at /docs (served from bundled assets, no external requests).
It exposes these 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 /openapi.yaml |
OpenAPI 3.1 description of every endpoint |
GET /docs |
Swagger UI for that document |
GET /api/process |
Raw process snapshot as JSON |
POST /api/execute |
Trigger an execution remotely; returns 202 with the new executionId |
GET /health/live |
Liveness probe. Returns 200 while the process is serving HTTP |
GET /health/ready |
Readiness probe. Returns 200 when persistence, schema migrations, plugins, and the executor are all UP, 503 with a per-check breakdown otherwise |
GET /health |
Summary: overall status, Nexora version, readiness checks, and capability circuit states (informational) |
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 |
GET /api/executions |
Execution history, newest first, cursor-paginated; filter by status, goal, from, to, limit |
GET /api/executions/{id} |
One execution with its step records (404 if unknown) |
DELETE /api/executions/{id} |
Cancel a running execution (200 cancelled, 404 unknown, 409 already finished) |
POST /api/executions/{id}/replay |
Replay a failed, timed-out, cancelled or compensated execution (202 with the new executionId, 409 otherwise) |
Note:
/health/liveand/healthwere added in v0.3.0. In v0.3.0/health/readychanged from reporting capability circuit state to reporting dependency readiness; circuit states are now undercapabilitiesinGET /health.
Every /api/* route and the live WebSocket require a key, sent as Authorization: Bearer <key>. /metrics, /health/*, the dashboard page, /openapi.yaml and /docs stay public for scrapers and probes.
- Server key (
nxk_...): printed once on first start (only its hash is saved), or set withNEXORA_API_KEYfor CI and containers. Rotate with--rotate-api-key. - User key (
nxu_...): minted when someone signs in with a username and password. The dashboard gets it as an HttpOnly cookie;nexora loginsaves it fornexora cancel/nexora replay. Valid 12 hours, logged under the user's name. Manage users withnexora user add/passwd/remove/list.
export NEXORA_API_KEY="$(openssl rand -base64 32)"
nexora observe
curl -H "Authorization: Bearer $NEXORA_API_KEY" http://localhost:9464/api/processThe payment pipeline demo prints a fresh server key in its console on every start and has no user accounts, so paste that key into its sign-in card. To try username and password sign-in, run the login demo: java -jar tests/login-demo/target/login-demo.jar (users alice / bob in tenants acme / globex, guided tour on its console).
See the Authentication guide for how each sign-in flow works, user management, the demo, and what 401 and 429 mean.
Without TLS the server logs a WARN on start, since keys, passwords and the session cookie cross the network in cleartext. To serve HTTPS (and WSS for the live stream) on your machine:
nexora tls-gen --out ./certs # a dev CA plus a localhost certificate
nexora observe --tls-cert ./certs/server.pem --tls-key ./certs/server-key.pem
curl --cacert ./certs/ca.pem https://localhost:9464/health/live
nexora login --server https://localhost:9464 --cacert ./certs/ca.pemImport certs/ca.pem as a trusted authority in your browser or OS to open the dashboard without warnings. In production, use a certificate from your CA or Let's Encrypt:
nexora observe \
--tls-cert /etc/letsencrypt/live/nexora.example.com/fullchain.pem \
--tls-key /etc/letsencrypt/live/nexora.example.com/privkey.pemA PKCS12 keystore works too: --keystore server.p12 with its password in NEXORA_KEYSTORE_PASSWORD. Only TLS 1.2 and 1.3 are accepted. The certificate is read at start, so restart after a renewal. See the HTTPS guide for trusting the CA per browser and OS, key formats, and troubleshooting.
Example execute request:
curl -H "Authorization: Bearer $NEXORA_API_KEY" -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_total- by tenantnexora_plan_duration_seconds- histogram by status (completed/failed) and tenantnexora_step_started_total,nexora_step_completed_total,nexora_step_failed_total- all by capability ID and tenantnexora_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 by tenant
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.
