Repository navigation
Releases
Nexora v0.5.0
Added
Deployment
- Kubernetes Helm chart and manifests -
deploy/helm/nexorarunsnexora observeon Kubernetes: Deployment, Service (HTTP 9464 and the dashboard's WebSocket on 9465), ConfigMap (nexora.jsonfromconfig), ServiceAccount, and optionally a Secret (for inline values), PersistentVolumeClaim, HorizontalPodAutoscaler, PodDisruptionBudget and Prometheus Operator ServiceMonitor. Startup and liveness probes hit/health/live, readiness/health/ready; resources default to requests250m/512Miand limits1/1Gi. Values cover the image, replicas, the API key Secret, persistence (h2on a PVC,jdbcfrom a Secret, ornone), the OTLP endpoint, akubernetes.io/tlsSecret for HTTPS, multi-tenancy and a users-file Secret. The pod runs as uid 10001 with a read-only root filesystem and no capabilities, and meets therestrictedPod Security standard. The chart refuses settings that would deploy but not work, such as H2 with more than one replica,jdbcwithout a URL, or TLS without a certificate.deploy/k8sholds the default rendering as plain YAML, generated bydeploy/k8s/generate.sh(#47) - Container image - A
Dockerfilepackagesnexora observeon the Temurin 21 JRE, non-root, with the heap sized to the container's memory limit. The newContainer imageworkflow builds it forlinux/amd64andlinux/arm64on everyv*tag and pushesghcr.io/iamvirul/nexora:<version>,:<major>.<minor>and:latest(#47) NEXORA_JDBC_URL- The CLI andnexora observecan use a database server: setNEXORA_JDBC_URLto a JDBC URL, credentials included. PostgreSQL 15+ (the driver now ships in the CLI jar) and H2 are accepted. The URL is read only from the environment, so it can come from a secret, and is never logged; setting it together withexecutionStorefails at startup (#47)
Distribution
- Published to Maven Central - The library modules (
nexora-core,nexora-plugin-spi,nexora-registry,nexora-tracing,nexora-tracing-otel,nexora-retry,nexora-event,nexora-executor,nexora-plugin-loader,nexora-planner,nexora-saga,nexora-persistence,nexora-runtime,nexora-api) are published to Maven Central with sources and javadoc JARs, all signed, so an application can depend on them with no extra repository. ThePublishworkflow builds, tests and uploads them as one bundle through the Central Publisher Portal on everyv*tag (mvn -P release deploy); Central publishes it only if every module passes validation. README and Getting Started show the Maven and Gradle snippets. The CLI and the LLM planner plugin JAR stay on GitHub Releases (#50)
Planner
- LLM planner plugin - New module
nexora-planner-llmplans goals with an LLM through the official SDKs: Anthropic (anthropic-java, default modelclaude-opus-5-5), OpenAI (openai-java), Gemini (Google'sgoogle-genai, on the Gemini Developer API), or any model on OpenRouter (the OpenAI SDK pointed at OpenRouter). It sends the goal, the capabilities the tenant may use and the names (not the values) of the intent context, and asks for the plan as structured JSON output. Every answer is validated before it runs: known capabilities, unique step ids, dependencies that exist and form no cycle, context names the request carries, output fields the producing capability declares, every required input bound. An invalid answer, a refusal, a truncated answer or a failed call, including an SDK error no client recognised, logs aWARNwith the reason and falls back to the rule-based planner. Valid plans are cached bysha256of the goal, the sorted capability ids and the context names, with a TTL (default 10 minutes) and a size bound. Anthropic requests on supported models opt into server-side refusal fallback. Configured withNEXORA_LLM_*environment variables ornexora.llm.*system properties (provider, model, max tokens, effort for Anthropic, temperature for OpenAI, OpenRouter and Gemini, timeout, retries, cache TTL, base URL, priority); keys come fromANTHROPIC_API_KEY,OPENAI_API_KEY,OPENROUTER_API_KEY,GEMINI_API_KEYor a key file, never from system properties. With nothing configured the plugin activates without a planner. Ships as a plugin JAR (-pluginclassifier, about 140 MB because of the three SDKs). Thetests/llm-planner-demosample runs a goal through both planners side by side, with--offlinefor a run without a key (#44)
Plugins
- Plugin hot reload -
NexoraEngine.reloadPlugin(pluginId, jar)(PluginManager.reloadPlugin) swaps an active JAR plugin for a new version without stopping the engine. The new JAR is loaded under a freshPluginClassLoaderand fully initialised before anything changes; then its capabilities and planners replace the old ones and new executions use them at once. Steps already running on the old version finish on it: every capability and planner from a JAR plugin is registered behind a ref-counted lease on its version, and the old version'sshutdown()and class loader close run when the last lease is returned. If the new JAR cannot be read, declares another id, needs an inactive plugin, fails to initialise or takes another plugin's capability id, nothing is swapped, the old version keeps serving,PluginReloadFailedEventis published andPluginInitializationExceptionis thrown; on successPluginReloadedEventcarries the old and new versions. Reload requires anExecutionStore(IllegalStateExceptionotherwise) and is refused for plugins registered in code.nexora plugins reload <pluginId> --jar <path>calls the newPOST /api/plugins/{pluginId}/reloadroute on a running observe server, which accepts only the server API key and only a.jarin the same directory as the plugin's current JAR.pluginsinnexora.jsonlists JARs to load at startup.NexoraEngine.pluginJar(pluginId)returns the JAR a plugin's active version came from (#43)
Documentation
- HTTP API reference on GitHub Pages - The docs site publishes the observe server's
openapi.yamlathttps://nexora.virul.me/api/openapi.yamlwith a rendered reference at/api/, copied fromnexora-clion every site build (website/scripts/copy-openapi.mjs), so the server and the site never drift. The new HTTP API page holds the versioning rules and an API changelog. The site redeploys whenopenapi.yamlchanges.
Changed
- Maven group ID is now
me.virul.nexora- All modules moved fromcom.nexoratome.virul.nexora, the namespace verified on Maven Central. Update thegroupIdin your build; artifact IDs and thecom.nexora.*Java packages are unchanged (#50) nexora-allis a BOM -nexora-allwas a shaded JAR bundling every module. It is now a bill of materials pinning every published module to one version: import it (<type>pom</type><scope>import</scope>, Gradleplatform(...)) and depend onnexora-api. The shaded JAR repeated classes that are also published as separate JARs (#50)- No more GitHub Packages - Releases no longer publish the Maven modules to GitHub Packages, which needs a token even to read; Maven Central replaces it. Versions up to 0.4.0 stay there under
com.nexora(#50) - Docs moved to nexora.virul.me - The documentation site is served from
https://nexora.virul.me. Links toiamvirul.github.io/nexora/redirect there - CLI startup errors are one line - When the engine cannot start (a store that cannot be opened, both
NEXORA_JDBC_URLandexecutionStoreset, an invalid pool size), the CLI printsError: <reason>and exits with code 2, instead of a stack trace and exit code 1. The reason keeps the root cause ("Connection refused"), with any JDBC URL cut tojdbc:<subprotocol>(#47) - CI validates the Helm chart - The
Helm chartworkflow runs on changes todeploy/**or theDockerfile:helm lint --strictwith the defaults and eachci/value set, checks that invalid settings are refused and thatdeploy/k8sis regenerated, then on a kind cluster runskubectl apply --dry-run=clientand--dry-run=serverfor every value set and installs the chart with an image built from the commit (#47) - Planners can decline after accepting an intent - A planner whose
plan()throws the newPlanningDeclinedException(innexora-plugin-spi) is skipped andCompositePlannertries the next planner, ending with the rule-based one, instead of failing the execution. OtherPlanningExceptions still fail planning (#44) - The OpenAPI document has its own version -
info.versioninopenapi.yamlis now the version of the API contract, separate from the Nexora release version, and changes only when the API does (it had stayed at0.3.0through the 0.4.0 release). CI fails when the currentinfo.versionhas no section in the API changelog. Theversionfield ofGET /healthis still the Nexora release. - Plugin planners are read live -
CompositePlannertakes aSupplier<List<Planner>>(new constructor) and the engine passes the active plugins' planners through it, so plugins activated or reloaded afterbuild()plan new intents.PluginManager.registeredPlanners()now returns an immutable snapshot that is replaced on every change. A JAR plugin's capabilities are registered wrapped, soCapabilityRegistry.findreturns a wrapper rather than the plugin's own instance. A capability or plan...
Nexora v0.4.0
Added
Persistence
- Schema migrations with Flyway -
JdbcExecutionStoreapplies versioned migrations fromdb/nexora/migrationduring construction, before any query, and records them innexora_schema_history. The location and table are namespaced so an application's own Flyway on the same database never picks them up.V1__initial_schema.sqlholds the previous DDL. A failed or edited migration makes construction throwIllegalStateExceptionnaming the script. Stores created by earlier releases are baselined at version 0 and adopted in place, keeping their data. Migrations run over separate connections when the store owns its pool; a caller-suppliedDataSourcemust allow at least two connections. Naming and authoring rules are inCONTRIBUTING.md(#29) - Step outputs are stored - Each completed step's output is saved with its step record and returned by
ExecutionStore.findById()asStepRecord.output(), aMap<String, Object>that is empty when the step returned nothing. A map output is kept as its JSON form and any other value goes undervalue;java.timevalues become ISO-8601 strings. MigrationV2__add_step_output.sqladdsoutput_jsonandoutput_truncatedtonexora_steps. Outputs larger thanpersistence.stepOutputMaxBytesinnexora.json(NexoraEngine.Builder.withStepOutputMaxBytes, default 64 KB) are kept as a truncatedpreview, flagged byStepRecord.outputTruncated()and logged atWARN, never rejected, and later steps still get the whole value.0stores no outputs. An output that cannot be written as JSON is logged and stored as empty, and the step still completes.GET /api/executions/{id}, the/api/processsnapshot (4 KB per step) and both dashboards show the outputs (#32) - Connection pooling for
JdbcExecutionStore- The store now borrows a connection per call from aDataSourceinstead of serialising every call on onesynchronizedconnection, so persistence calls run in parallel up to the pool size. NewJdbcExecutionStore(DataSource)constructor for caller-managed pools (left open onclose()),JdbcExecutionStore(String, PoolConfig)andh2AutoServer(String, PoolConfig)for a store-owned HikariCP pool, andHikariCPDataSourceFactory. The CLI readspersistence.pool.maximumPoolSize,minimumIdle,connectionTimeoutMsandidleTimeoutMsfromnexora.json(#28) - PostgreSQL integration tests -
JdbcExecutionStorePostgresITruns the store against PostgreSQL 15 in a Testcontainers container: Flyway migrations on a fresh database, execution and step CRUD,findById,findRecent, history filtering with cursor paging, webhook deliveries, dead letters, schedules, 10 concurrent writers, and racing upserts on a new step. Tests tagged@Tag("integration")run under failsafe inmvn verifyand are excluded frommvn test;-DskipITsand-DskipUTspick one side. CI runs them as a separate step limited to 3 minutes. They need Docker (#45)
Security
-
Rate limiting for the observe server - Each client gets 60 requests a minute across the authenticated
/apiroutes and 10 a minute toPOST /api/executeon top (token buckets, Bucket4j). More get429with{"error":{"code":"RATE_LIMIT_EXCEEDED","message":"...","retryAfterSeconds":N}}and an accurateRetry-After, are logged atWARNwith client IP, method, route and limit, and counted innexora_rate_limit_violations_total{endpoint,client_ip}. Limits apply after authentication, so unauthenticated requests get401without spending tokens, and never to health probes,/metrics, the dashboard or the docs. Configured underobserve.rateLimitinnexora.json(enabled,globalRpm,executeRpm,trustedProxies);enabled: falseapplies no limit at all.X-Forwarded-Foris honoured only from listed proxies, taking the rightmost untrusted hop; each listed proxy must append the peer address it saw, or a client could write that hop itself. Idle buckets are dropped and at most 100,000 clients are tracked per limit.RequestRateLimiter,RateLimitConfigandClientAddresslive incom.nexora.api.ratelimitfor other embedded servers;ObserveServer.Config.withRateLimitsets the limits (#49) -
Multi-tenancy - Every execution belongs to a tenant:
Intent.withTenantId("acme")(or the new constructor),"default"when none is given, exposed to capabilities asCapabilityRequest.tenantId()and to the engine asExecutionContext.tenantId(). Ids are 1 to 64 characters ofa-z,0-9,-,_(TenantId). MigrationV3__add_tenant_id.sqladdstenant_id(default'default') to executions, steps, webhook deliveries, dead letters and schedules, with existing rows indefault; steps and deliveries copy their execution's tenant. Dead letters, schedules and replays keep the original tenant. Tenant-scoped API:NexoraEngine.replay(tenantId, id),cancel(tenantId, id),listSchedules(tenantId),cancelSchedule(tenantId, id),listCapabilities(tenantId),ExecutionStore.findById(tenantId, id),findDeadLetters(tenantId, ...),ExecutionFilter.forTenant; another tenant's work answers like an unknown id.NexoraEngine.Builder.withTenantIdbinds an engine to one tenant: intents without a tenant run for it, others are refused, and its cron scheduler only runs that tenant's schedules.CapabilityDescriptor.withTenantIdsrestricts a capability to some tenants; others neither plan with it nor invoke it (CAPABILITY_NOT_FOUND).nexora observe --multi-tenantscopes every/apiroute and the live stream to one tenant taken from the credential: users are bound to a tenant (nexora user add --tenant,nexora user tenant), and a header naming another tenant gets403 TENANT_FORBIDDEN; the server API key must sendX-Tenant-ID(400 TENANT_REQUIRED), also when opening a dashboard session, which stays on that tenant.nexora cancel/replaytake--tenant. Without the flag nothing changes and the header is ignored. Execution metrics carry atenantlabel (nexora_active_executionsis now per tenant; sum it for the total), andnexora dlq/nexora scheduletake--tenant. Schedule firings are claimed in the store before they run (ExecutionStore.claimScheduleFire, a compare-and-set onnext_fire_at), so engines sharing a store, bound, unbound or replicas, fire each slot exactly once. PostgreSQL row-level security for direct database readers is documented inwebsite/docs/concepts/multi-tenancy.md(#48) -
Authentication for the observe server - Every
/api/*route and the live WebSocket stream require an API key, sent asAuthorization: Bearer <key>. A missing or wrong key gets401with{"error":{"code":"UNAUTHORIZED","message":"..."}}andWWW-Authenticate: Bearer, before any other check, so unknown/apipaths do not reveal what exists./metrics,/health/*, the dashboard page,/openapi.yamland/docsstay public. On first startnexora observegenerates a key, prints it once and saves only its SHA-256 digest (owner-only,~/.nexora/observe-api-key.sha256, override with--api-key-file);NEXORA_API_KEYor--api-key(at least 32 characters) supplies one instead, with nothing printed or written.--rotate-api-keyreplaces it. Keys are compared as SHA-256 digests withMessageDigest.isEqual, so timing reveals nothing. The dashboard trades the key once for anHttpOnly; SameSite=Strictsession cookie (POST /api/session,DELETE /api/sessionto log out), and cookie-authenticated state changes and WebSocket handshakes must also carry the dashboard's ownOrigin.nexora cancelandnexora replaysend the key fromNEXORA_API_KEYor--api-keyand refuse to send a key or password over plain HTTP to another machine unless--allow-insecure-httpis passed. The payment pipeline demo uses the same auth with a key printed per run (nothing saved), and keeps its/api/mock-webhookreceiver open for signed webhook deliveries. The auth classes live innexora-api(com.nexora.api.auth:ApiAuth,ApiKeyStore,UserStore,PasswordHasher,SessionEndpoints) so any embedded HTTP server can reuse them (#30) -
Username and password login with user API keys -
nexora user add/passwd/remove/listmanages dashboard accounts in an owner-only~/.nexora/observe-users(--users-file), hashed with PBKDF2-HMAC-SHA256 at 600,000 iterations and a per-user salt; passwords are 12 to 128 characters. A correct password mints a user API key (nxu_..., 12 hours) that works on every route the server's key does (except opening a dashboard session) and is logged under the user's name.POST /api/keysreturns it in the body fornexora login, which saves it to~/.nexora/credentialsfornexora cancel/nexora replay;POST /api/sessiongives the dashboard the same key as the HttpOnly cookie instead.GET /api/sessionreports who is signed in, andDELETE /api/session(nexora logout) revokes it. Removing a user or changing a password revokes their keys on the running server, with no restart. Five failures lock a username for 15 minutes and each client is limited to 20 attempts a minute, checked before any hashing; unknown usernames get the same treatment and timing, so nothing reveals whether an account exists. Login must beapplication/jsonand same-origin. The dashboard signs in with a username and password by default, with the API key as an option; the payment pipeline demo stays key-only (#30) -
Login demo - New
tests/login-demosample runs the real observe server onlocalhost:9470with demo usersaliceandbob, a server...
Nexora v0.3.0
Added
Core Engine
- Execution cancellation -
NexoraEngine.cancel(executionId)returnsCompletableFuture<Boolean>(trueif cancelled,falseif already finished; fails withExecutionNotFoundExceptionfor unknown ids). Pending steps are never started, running steps are interrupted, the execution is persisted asCANCELLED, andPlanCancelledEventfires. Saga compensation runs for completed steps when enabled. Cancelled executions are not dead-lettered. NewExecutionStatus.CANCELLEDandExecutionState.CANCELLED(#33)
Observability
NexoraEngine.submit(intent)- Starts an execution and returns anExecutionHandle(executionId+ result future) immediately, so the id is known before the run finishes (#33)- OpenAPI specification for the observe server - A hand-maintained OpenAPI 3.1 document covering every endpoint, with schemas and success and error examples, served at
GET /openapi.yaml. Swagger UI is served atGET /docsfrom the bundledorg.webjars:swagger-uiassets under a strict Content-Security-Policy, with no CDN or validator calls. CI lints the document with Redocly CLI, andOpenApiSpecTestfails the build when a route is added without documenting it or the document describes a route the server does not serve (#46) - Execution replay -
NexoraEngine.replay(executionId)re-runs aFAILED,TIMED_OUT,CANCELLEDorCOMPENSATEDexecution with the same goal and context under a new execution id; the original id is in the replay's context as_replayOf. Steps that completed reuse their original idempotency key so idempotent capabilities skip repeated side effects; failed steps, and every step of a compensated execution, get fresh keys. Exposed asPOST /api/executions/{id}/replay(202,404,409with the current state),nexora replay <executionId>, and a Replay button in the history panel. Dead letter replay now delegates to it (#37) - Execution history REST API -
GET /api/executionslists executions newest first with keyset (cursor) pagination andstatus,goal,from/to,limitfilters, responding{"data": [...], "cursor": "...", "total": N}.GET /api/executions/{id}returns one execution with its step records.ExecutionStoregainsfindByFilter(ExecutionFilter)andcount(ExecutionFilter); the JDBC store builds both with bound parameters only and adds an index on(started_at, execution_id). The observe UI and the payment pipeline demo get an Execution History panel with status and goal filters, cursor-based Load more, and click-to-expand step detail (#34) - Cancel REST API, CLI, and UI -
POST /api/executeand DLQ replay now return the newexecutionId. The observe UI has a Cancel button on running executions, and the payment pipeline demo adds a cancellation scenario and a "Slow payment" preset.DELETE /api/executions/{id}on the observe server (200cancelled,404unknown,409already finished) andnexora cancel <executionId> [--server URL]. Newnexora_plan_cancelled_totalcounter; cancels are not counted as failures (#33) - Health check endpoints -
GET /health/live(liveness),GET /health/ready(readiness: persistence, plugins, executor;503with a per-check breakdown when any is DOWN), andGET /health(summary with version, checks, and capability circuit states). All three are unauthenticated. Newnexora_readygauge andNexoraNotReadyalert (#31)
Changed
-
Code formatting enforced - All Java code is formatted with palantir-java-format via the Spotless Maven plugin.
mvn verifyfails on unformatted code; runmvn spotless:applyto fix -
/health/readysemantics - Readiness now reflects the engine's own dependencies instead of capability circuit state. An OPEN circuit no longer returns503; circuit states moved to thecapabilitiesfield ofGET /health(#31) -
PluginManager.loadPlugin(Path)returns the plugin id - Previouslyvoid. Source compatible; code compiled against an oldernexora-plugin-loadermust be recompiled (#140) -
DagStepScheduler.schedule(Plan, ExecutionContext, AtomicBoolean)replaced byschedule(Plan, ExecutionContext, HaltSignal)-HaltSignalcarries why the execution stopped (DEADLINEorCANCELLED) so the scheduler can reportTIMED_OUTorCANCELLED. Deadline behaviour is unchanged (#33) -
Default webhook events include
CANCELLED- Intents without explicitwebhookEventsare now notified when cancelled (#33)
Fixed
- Step records could be persisted out of order - Step persistence ran as asynchronous event-bus handlers with no ordering, so a step's completion could be written before its start (leaving it
RUNNING), and step rows could still be missing when the execution's result completed. Step persistence now uses the newExecutionEventBus.subscribeSync, which runs inline in publish order; the step-start row, with its idempotency key, is written before the capability runs (#37) - Execution state write failures were silent -
JdbcExecutionStore.updateExecutionswallowed SQL errors, so a failed terminal write could leave a record stuck atCOMPENSATING(blocking replay) or atFAILEDafter compensation. It now throws, and the engine retries state writes up to three times. The engine also keeps an execution tracked until saga compensation has finished, so replay and cancel never act on a half-written record (#37) - Step idempotency keys were erased on completion -
JdbcExecutionStore.upsertStepreplaced the whole row, and completion and failure updates carry no key, so the key recorded at step start was lost as soon as the step finished. Updates without a key now keep the recorded one (#37) findByIdreported database errors as "not found" -JdbcExecutionStore.findByIdswallowed query failures and returned empty, so a database outage surfaced as404from the detail endpoint and asNOT_FOUNDfromcancel. It now throwsIllegalStateException; both endpoints answer500(#34)- Observe server failed to serialize
java.timevalues -nexora-clilackedjackson-datatype-jsr310, so any response containing anInstantorDurationthrew instead of responding:GET /api/dead-letterswith entries, webhook delivery records, andGET /healthonce capabilities were registered. Instants serialize as ISO-8601 timestamps and durations as ISO-8601 durations (PT0.25S) (#34) - Timed-out capabilities kept running -
TimeoutInterceptorusedCompletableFuture.cancel(true), which does not interrupt the worker, so a capability that exceeded its timeout carried on in the background. It now runs on aFutureTaskand is interrupted on timeout (#33) - Retries after interruption -
RetryInterceptorno longer starts another attempt once the step thread has been interrupted (#33) - Plugin jars never activated -
NexoraEngine.Builder.withPluginJar()loaded the jar but never activated the plugin, so its capabilities and planners were never registered and executions failed withCAPABILITY_NOT_FOUND.build()now activates jar plugins after inline plugins, in the order added, and fails withPluginInitializationExceptionif activation fails (#140)
Nexora 0.2.1
Update
- Dependency Update - Bumps ch.qos.logback:logback-classic from 1.5.35 to 1.5.37.
Nexora v0.2.0
Added
Core Engine
- Cron Scheduling - Recurring intent executions driven by 5-field UNIX cron expressions.
NexoraEngine.schedule(cron, intent)persists the schedule to anexora_schedulestable and returns aScheduledExecutionhandle withid(),nextFireTime(), andcancel(). Three missed-fire policies control restart behaviour:SKIP(drop missed windows),FIRE_ONCE(fire once to catch up, default), andFIRE_ALL(fire once per missed window).ScheduledExecutionFiredEventandScheduledExecutionMissedEventare published on the event bus. Requires a persistence store (#38) - Dead Letter Queue - Permanently failed executions (after all retries are exhausted) are now captured in a
nexora_dead_letterstable. Each dead letter record storesexecutionId,goal,context,failureCode,failureMessage,failedAt, and areviewState(PENDING/RESOLVED/REPLAYED). AnExecutionDeadLetteredEventfires on the event bus for external alerting (#42) - Conditional Branching - Allows defining a
StepCondition(e.g.ContextValueEquals,StepOutputEquals) on any step. The scheduler dynamically evaluates these before step execution and skips the step if the condition resolves tofalse(#41) - Plan-level execution deadline / timeout - wall-clock deadline that cancels an entire plan execution when the limit is exceeded, returning
TIMED_OUTexecution status and triggering full saga compensation; overridable perIntentor globally viaNexoraEngine.Builder.withDefaultPlanDeadline()(#35)
Executor
- Capability contracts enhancements - Added
openDurationandprobeIntervalproperties toCapabilityContractto support stateful circuit breaking (#36) - Capability circuit breaker -
CapabilityContractMonitornow implements a robust circuit breaker state machine (CLOSED, OPEN, HALF_OPEN) and routes unhealthy capabilities to their fallback transparently (#36)
Observability
- Schedule REST API - Three new endpoints on the observability server:
GET /api/schedules(list all schedules),POST /api/schedules(register a schedule withcronExpression,goal, and optionalmissedFirePolicy),DELETE /api/schedules/{id}(cancel immediately). The observe dashboard includes a live Cron Schedules panel with an add-form and auto-refresh every 30 seconds (#38) - DLQ REST API - Three new endpoints on the observability server:
GET /api/dead-letters(paginated, filterable bystate; defaults toPENDING),POST /api/dead-letters/{id}/replay(creates a new execution and marks the entry asREPLAYED),POST /api/dead-letters/{id}/resolve(accepts an optionalreasonbody and marks the entry asRESOLVED). Authentication will be enforced once #30 lands (#42) - Circuit breaker monitoring - Added
/health/readyendpoint to the observability server to monitor capabilities circuit statuses (#36) - Webhook Callbacks - Implemented asynchronous terminal event webhooks using Java 11
HttpClientwith exponential backoff. Webhooks are signed with an HMAC-SHA256 signature and delivery attempts are persisted innexora_webhook_deliveriesand exposed via/api/webhook-deliveries/{executionId}(#39)
CLI
nexora schedule- New subcommand group for cron schedule management:nexora schedule add --goal "<goal>" --cron "<expr>" [--policy SKIP|FIRE_ONCE|FIRE_ALL],nexora schedule list [--active-only],nexora schedule remove <id>. RequiresexecutionStoreset innexora.json(#38)nexora dlq- New subcommand group for dead letter queue management:nexora dlq list [--state PENDING|RESOLVED|REPLAYED|ALL],nexora dlq replay <id>,nexora dlq resolve <id> [--reason "..."](#42)
Fixed
- Observability UI gets stuck on RUNNING for timed-out plans -
NexoraObservabilitytracker missed thePlanTimedOutEvent, leaving the UI infinitely spinning onRUNNING. Fixed by adding the event listener and updating the dashboard UI to correctly render the orangeTIMED_OUTbadge.
Nexora v0.1.0
Added
Core Engine
- Initial implementation of the Intent-Based Execution Engine (IBEE)
nexora-core-Intent,Plan,Step,InputBinding,CapabilityRequest,CapabilityResult,ExecutionContext,TraceContextnexora-plugin-spi-NexoraPlugin,CapabilityProvider,Capability,CapabilityDescriptor,CapabilityContract,Plannerinterfacesnexora-planner-RulePlannermatching step definitions to goals via predicate matchers;PlanRegistryfor step definition storagenexora-event- sealedExecutionEventhierarchy;InProcessEventBuswith typed subscriptionsnexora-registry-DefaultCapabilityRegistrynexora-retry-RetryPolicy,RetryPolicyRegistry,ExponentialBackoffPolicynexora-tracing-TracerSPI,NoopTracernexora-runtime-ExecutionEngineorchestrating planner + scheduler + event busnexora-api-NexoraEnginepublic facade with fluent builder
Executor
DagStepSchedulerexecutes plan steps in parallel respecting declareddependsOnrelationships; cycle detection via DFS before execution startsInterceptorPipelineandExecutionInterceptorSPI for wrapping capability invocationsTracingInterceptorpropagatesTraceContextthrough all capability callsCapabilityInvokeras the terminal pipeline node; resolves capabilities from the registryRetryInterceptorandTimeoutInterceptorwired into the default interceptor pipeline
Plugins
- Plugin loader - loads
NexoraPluginimplementations from external JARs viaURLClassLoader; manages plugin lifecycle (register → activate → deactivate → unload) - Pluggable planner SPI - plugins can supply custom
Plannerimplementations;CompositePlannertries plugin planners first and falls back to the rule-based planner - Reactive plan amendments -
CapabilityResultcan returnPlanAmendmententries;DagStepSchedulerappliesAddStepAmendment,SkipStepAmendment, andModifyInputAmendmentatomically before dependent steps start - Capability contracts -
CapabilityContractdeclares p99 latency SLA, max error rate, and fallback capability ID;CapabilityContractMonitormaintains a sliding window and routes unhealthy capabilities to their fallback transparently
CLI
nexoraCLI -run,demo,observe, andhelpsub-commands via picocliEngineFactory- buildsNexoraEnginefrom aCliConfigYAML file
Observability
- Observability server (
nexora observe) - embedded HTTP server serving a live process dashboard, Prometheus/metricsendpoint, and/api/processsnapshot endpoint NexoraObservability- attaches to anyNexoraEnginevia event subscriptions; tracks execution lifecycle, step latencies, error rates, and amendment counts using Prometheus counters/gauges/histogramsProcessSnapshotandExecutionSnapshotDTOs for the process view API- WebSocket push -
SnapshotListenerSPI broadcasts liveProcessSnapshotJSON to connected browser clients over a dedicated port;ObserveCommandinjects the port into the bundled HTML at serve time - Live observe UI - professional dark theme, Gantt-style timeline showing step parallelism, viewport-height layout with the execution list as the sole scrollable area, mobile responsive, live "Updated" counter
- Grafana stack integration (docker-compose) with a pre-built dashboard
Production Readiness
- Idempotency keys -
CapabilityRequestcarries a UUID per step-execution attempt; propagated throughStepStartedEventso capabilities can deduplicate side effects on retry - Durable execution state -
ExecutionStoreSPI with a JDBC/H2 default; persists execution lifecycle and per-step start/complete/fail transitions; opt in viaNexoraEngine.builder().withExecutionStore(...) - Contract enforcement - capability p99 SLA used as the effective
TimeoutInterceptordeadline when no explicit step timeout is set - Orchestration-based saga -
SagaOrchestratorruns compensating capabilities in reverse topological order on partial execution failure;Step.compensateCapabilityIddeclares the rollback capability; opt in via.withSagaEnabled(true) CompensationStartedEvent,CompensationCompletedEvent,CompensationFailedEventadded to the sealedExecutionEventhierarchy
Distribution
nexora-all- shaded bundle module aggregating all library modules (nexora-api,nexora-common,nexora-core,nexora-plugin-spi,nexora-registry,nexora-tracing,nexora-retry,nexora-event,nexora-executor,nexora-plugin-loader,nexora-planner,nexora-capabilities,nexora-saga,nexora-runtime,nexora-persistence) into a single deployable artifact; external transitive dependencies are promoted; individual modules are published separately alongside the bundle
Samples
tests/payment-pipeline- standalone 8-step fraud-detection DAG (validate_request,enrich_user_data,check_velocity,screen_sanctions→run_fraud_check→process_payment→send_confirmation+update_ledger) demonstrating plan amendments, capability contracts, fallback routing, and orchestration-based saga compensationscreen_sanctionsstep added betweencheck_velocityandrun_fraud_check; fails withSANCTIONS_HITwhenforceBlockedUser=true;screen_sanctions_compensateincluded in the saga rollback chain- Four startup scenarios: happy path, high-risk fraud review, gateway failure with saga, and sanctions blocklist hit with partial compensation
CI/CD
ci.yml- builds and runs all tests on every push and pull request; uploads Surefire reports on failurepublish.yml- publishes each Maven module individually to GitHub Packages in dependency order, then creates a versioned GitHub Release onv*tag push; pre-release flag set automatically for tags containing-bump-version.yml- triggered automatically after a release is published; computes the next SNAPSHOT version, updates allpom.xmlfiles viamvn versions:set, and opens a pull request to merge the version bump back to the default branch
Fixed
CapabilityRequestrejects null inputs - optional context bindings (e.g.forceFailure,forceVelocityFail,forceBlockedUser) resolve tonullwhen the caller omits them;Map.copyOf()throwsNullPointerExceptionon null values, causing steps to fail with an internal error and leaving dependent steps permanently pending. Null-valued entries are now stripped before the immutable copy is taken; capabilities should usegetOrDefault()orBoolean.TRUE.equals()to test absent flags.DagStepSchedulerleaves dependent steps permanently pending on unexpected exceptions - when a step'sthenApplyAsyncthrew an unchecked exception the future completed exceptionally, propagating throughCompletableFuture.allOfand blocking all downstream steps indefinitely. A.handle()stage now intercepts any exception, publishes aStepFailedEventwith codeINTERNAL_ERROR, and returns a failureStepResultso the pending counter decrements correctly and the execution terminates.run_fraud_checkembedded a null-proneforceFailureflag in itsMap.of()output - removed; it was a control input, not a produced output.