Skip to main content

nexora-persistence

Durable execution state storage. The default implementation uses H2 over JDBC. Swap it out for any SQL or NoSQL backend by implementing ExecutionStore.

ExecutionStore interface

public interface ExecutionStore extends AutoCloseable {
void createExecution(ExecutionRecord record);
void updateExecution(String executionId, ExecutionState state, Instant completedAt);
void upsertStep(String executionId, StepRecord record);
void recordWebhookDelivery(WebhookDeliveryRecord record);
List<WebhookDeliveryRecord> getWebhookDeliveries(String executionId);
Optional<ExecutionRecord> findById(String executionId);
List<ExecutionRecord> findRecent(int limit);

// Dead Letter Queue (default methods — Unreleased)
default void createDeadLetter(DeadLetterRecord record);
default Optional<DeadLetterRecord> findDeadLetterById(String id);
default List<DeadLetterRecord> findDeadLetters(DeadLetterReviewState state, int offset, int limit);
default void updateDeadLetterState(String id, DeadLetterReviewState state, String resolveReason);

void close();
}

Idempotency: upsertStep must be safe to call twice with the same (executionId, stepId, idempotencyKey). The JDBC implementation uses an INSERT OR REPLACE (H2) / ON CONFLICT DO UPDATE (PostgreSQL) strategy.

The four DLQ methods are default methods that throw UnsupportedOperationException. Existing custom ExecutionStore implementations continue to compile without change. Override them to enable DLQ support.


Default H2 store

Enabled automatically when no custom store is configured. Uses an in-process H2 database with Flyway-managed schema. Suitable for local development and testing.

Schema is located at nexora-persistence/src/main/resources/db/migration/.


Connecting the CLI observer

The embedded HTTP server in nexora-cli serves execution history from the store. See CLI Reference for the observe command.


PostgreSQL example

Add a JDBC driver and implement ExecutionStore:

public class PostgresExecutionStore implements ExecutionStore {

private final DataSource ds;

@Override
public void createExecution(ExecutionRecord record) {
try (var conn = ds.getConnection();
var ps = conn.prepareStatement(
"INSERT INTO executions(id, goal, state, started_at) VALUES (?,?,?,?)")) {
ps.setString(1, record.executionId());
ps.setString(2, record.goal());
ps.setString(3, record.state().name());
ps.setObject(4, record.startedAt());
ps.executeUpdate();
}
}

// ... implement remaining methods

@Override
public void close() { /* close data source if owned */ }
}

Register it on the engine:

NexoraEngine engine = NexoraEngine.builder()
.withExecutionStore(new PostgresExecutionStore(dataSource))
.build();

ExecutionRecord fields

FieldTypeDescription
executionIdStringUUID assigned at execution start
goalStringThe intent goal string
stateExecutionStateRUNNING, COMPLETED, FAILED
startedAtInstantExecution start timestamp
completedAtInstantExecution end timestamp (null if still running)
stepRecordsList<StepRecord>One entry per executed step