JamJet
Integrations

Spring Boot Starter

Record Spring AI ChatClient calls on the JamJet runtime, with audit and approval advisors. Check the release status before you install.

Spring Boot Starter

This guide covers the JamJet Spring Boot integration: what each advisor does, how to test agents, and how to monitor them.

Release status

0.1.0 is the only release of this starter on Maven Central, and it does not reach the runtime. Its client calls /api/v1/... routes that the runtime does not serve. The health check passes. Every other call fails, so the advisors log JamJet runtime unavailable, proceeding without durability and let the call through. With 0.1.0, nothing is recorded, no call is held for approval and no audit event is written. The fix is merged in jamjet-spring but not released yet. The rest of this page describes the integration as designed.


Why durability matters for AI agents

Spring AI gives you a clean abstraction for building LLM-powered applications. You get ChatClient, advisors, tool calling, and model portability. What you do not get is any protection when things go wrong at runtime.

Consider what happens when your Spring AI agent is halfway through a multi-step task --- it has called a search tool, retrieved results, and is about to synthesize an answer --- and the process crashes. With vanilla Spring AI, the entire interaction is lost. The user sees an error. The tokens you already spent are wasted. There is no record of what happened.

JamJet records each Spring AI call as an execution on the runtime, with events for the prompt, the response and the outcome. The model call runs in your JVM and the runtime holds the record. The runtime does not re-run, resume or replay Spring AI calls, so a call that was in flight when your process stopped is not continued after a restart.

The record is what the other features build on:

  • Audit trails --- every prompt, response, tool call, and token count recorded as immutable events. Required for regulated industries (financial services, healthcare, legal).
  • Human-in-the-loop approval: hold a marked call until a human approves or rejects it. The wait happens in your JVM, so it does not survive a restart.
  • Replay testing --- replay a production execution in a test environment and assert on the results. No LLM calls needed.
  • Cost tracking --- aggregate real token costs per execution, per user, per workflow.

For deeper background on why we built JamJet and the problems it solves, see Why We Built JamJet.


Setup

1. Add the dependency

The starter is published on Maven Central. Add a single dependency and Spring Boot auto-configuration handles the rest.

Maven

<dependency>
    <groupId>dev.jamjet</groupId>
    <artifactId>jamjet-spring-boot-starter</artifactId>
    <version>0.1.0</version>
</dependency>

Gradle (Kotlin DSL)

implementation("dev.jamjet:jamjet-spring-boot-starter:0.1.0")

Gradle (Groovy DSL)

implementation 'dev.jamjet:jamjet-spring-boot-starter:0.1.0'

2. Start the JamJet runtime

The runtime is the execution engine that stores events and manages workflow state. Run it with Docker:

docker run -p 7700:8080 -e JAMJET_DEV_MODE=1 jamjetdev/jamjet:0.5.0

The image is built for linux/amd64; on Apple silicon add --platform linux/amd64. The server listens on 8080 inside the container, so this maps it to localhost:7700. JAMJET_DEV_MODE=1 turns off authentication, so use it only on your own machine. The image keeps state in memory.

Or, if you have the CLI installed:

jamjet dev

3. Configure

Add the runtime URL to your application.yml:

spring:
  jamjet:
    runtime-url: http://localhost:7700
    # api-token: ${JAMJET_API_TOKEN}      # optional, for authenticated runtimes
    # tenant-id: default                   # tenant label (does not isolate tenants)
    durability-enabled: true               # default: true
    connect-timeout-seconds: 10            # default: 10
    read-timeout-seconds: 120              # default: 120

Or in application.properties:

spring.jamjet.runtime-url=http://localhost:7700

What auto-configuration does

When JamjetAutoConfiguration detects Spring AI's ChatClient on the classpath and spring.jamjet.durability-enabled=true (the default), it registers the following beans:

BeanConditionPurpose
JamjetRuntimeClientAlways (when durability enabled)HTTP client to the JamJet runtime
JamjetDurabilityAdvisorAlways (when durability enabled)Wraps every ChatClient call with durable execution
ChatClientCustomizerAlways (when durability enabled)Auto-injects the durability advisor into all ChatClient instances
JamjetAuditAdvisorspring.jamjet.audit.enabled=true (default)Records prompts, responses, and token usage as audit events
JamjetAuditServicespring.jamjet.audit.enabled=true (default)Programmatic access to audit trail
JamjetApprovalAdvisorspring.jamjet.approval.enabled=true (opt-in)Pauses execution for human approval
JamjetApprovalControllerApproval enabled + web applicationREST endpoints at /jamjet/approvals
JamjetMicrometerBridgeMicrometer on classpath (default)Publishes execution metrics
JamjetOtelBridgespring.jamjet.observability.opentelemetry=true (opt-in)OpenTelemetry span creation

The durability advisor is injected via a ChatClientCustomizer, so you do not need to add it manually. Every ChatClient you build from the auto-configured ChatClient.Builder gets durability for free.

Graceful degradation

If the JamJet runtime is unavailable --- network partition, container not started, authentication failure --- the JamjetDurabilityAdvisor logs a warning and lets the request proceed without durability. Your application never fails because of JamJet being down. This is intentional: durability is a safety net, not a single point of failure.


Durability Advisor

The JamjetDurabilityAdvisor is the core of the integration. It implements Spring AI's BaseAdvisor interface and intercepts every ChatClient call to wrap it with durable execution.

Event sourcing for Spring developers

If you have not worked with event sourcing before, here is the essential idea: instead of storing only the current state of an operation, you store every event that led to that state. The current state is a derived view --- you can always reconstruct it by replaying the events from the beginning.

For the Spring integration, this means each ChatClient call is recorded as an execution with immutable events on the JamJet runtime. The event log is the record of what happened.

It does not give you crash recovery for Spring AI calls. The model call runs in your JVM, and the runtime does not re-run or resume it.

What happens on a crash

If the process stops while a ChatClient call is in flight, that call is lost, as it would be without JamJet. Its execution stays on the runtime without an outcome event, so you can see that it started and never finished. After a restart, the next call starts a new execution. Nothing is replayed and no step is skipped.

Before and after

The key insight: your application code does not change. Durability comes from the advisor, not from your business logic.

Without JamJet (vanilla Spring AI):

@Bean
ChatClient chatClient(ChatClient.Builder builder) {
    return builder.build();
}

@Bean
CommandLineRunner demo(ChatClient chatClient) {
    return args -> {
        String result = chatClient.prompt("Summarize AI trends")
                .call()
                .content();
        System.out.println(result);
        // If the process crashes here, everything is lost
    };
}

With JamJet (same code, durability comes from auto-configuration):

@Bean
ChatClient chatClient(ChatClient.Builder builder) {
    return builder.build(); // JamjetDurabilityAdvisor auto-injected
}

@Bean
CommandLineRunner demo(ChatClient chatClient) {
    return args -> {
        String result = chatClient.prompt("Summarize AI trends")
                .call()
                .content();
        System.out.println(result);
        // Recorded on the runtime as an execution; not re-run after a crash
    };
}

The only difference is the dependency on your classpath. The ChatClientCustomizer registered by JamjetAutoConfiguration adds the JamjetDurabilityAdvisor to every ChatClient.Builder automatically.

Context keys

The durability advisor uses three context keys to track execution state:

KeyTypeDescription
jamjet.execution.idStringUnique execution ID assigned by the runtime
jamjet.workflow.idStringWorkflow ID (derived from the compiled IR)
jamjet.session.idStringSession ID for grouping related interactions

You can read these from the response context for logging, correlation, or downstream processing.

For more on the agentic patterns that durability enables --- ReAct loops, plan-and-execute, critic chains --- see Agentic AI Patterns.


Audit Advisor

The JamjetAuditAdvisor records every prompt, response, and token usage as immutable audit events in the JamJet runtime. It runs in the advisor chain after the durability advisor (order LOWEST_PRECEDENCE - 50), so every audit entry is associated with a durable execution ID.

Why audit trails matter

If you are building AI agents for enterprise use --- financial services, healthcare, insurance, legal --- you face regulatory requirements around recordkeeping. Regulators want to know:

  • What prompt was sent to the model?
  • What did the model respond?
  • How many tokens were consumed (and at what cost)?
  • Which user initiated the interaction?
  • Can you reproduce this interaction six months from now?

Without an audit trail, you cannot answer any of these questions. The JamjetAuditAdvisor answers all of them by default.

Audit event structure

Each audit entry is persisted as an external event on the execution. Here is what a prompt audit event looks like:

{
  "type": "prompt",
  "advisor": "JamjetAuditAdvisor",
  "content": "Analyze the risk profile of portfolio XYZ-1234"
}

And the corresponding response event:

{
  "type": "response",
  "advisor": "JamjetAuditAdvisor",
  "content": "Based on the current allocation, portfolio XYZ-1234 has...",
  "prompt_tokens": 847,
  "completion_tokens": 1203,
  "total_tokens": 2050
}

Configuration

Audit is enabled by default. You can control what gets logged:

spring:
  jamjet:
    audit:
      enabled: true              # default: true
      include-prompts: true      # default: true — log full prompt text
      include-responses: true    # default: true — log full response text

For regulated environments where you need the audit trail but must not persist PII or sensitive prompt content:

spring:
  jamjet:
    audit:
      enabled: true
      include-prompts: false     # omit prompt text from audit events
      include-responses: false   # omit response text from audit events

This still records the fact that an interaction happened (with the execution ID, timestamp, and token counts) without storing the actual content. For more on PII handling and data governance in agent systems, see Data Governance and PII Retention.


Human-in-the-loop approval

The JamjetApprovalAdvisor holds a marked call until a human approves or rejects it through a REST endpoint, then lets it continue or aborts it. The call waits in your JVM, so a restart loses it. Because the pause is backed by the durable execution engine, it survives process restarts.

When to use approval gates

Approval gates are for high-stakes actions where you want a human to review the agent's plan before it executes:

  • Financial transactions above a threshold
  • Customer-facing communications
  • Database mutations in production
  • Actions with legal or compliance implications

Enabling approval

Approval is opt-in (disabled by default):

spring:
  jamjet:
    approval:
      enabled: true
      webhook-url: https://hooks.slack.com/services/T.../B.../xxx  # optional
      timeout: 30m            # default: 30m (supports s, m, h suffixes)
      default-decision: rejected   # default: rejected — what happens on timeout
PropertyDefaultDescription
spring.jamjet.approval.enabledfalseEnable approval workflow
spring.jamjet.approval.webhook-url---External webhook for notifications (Slack, email, etc.)
spring.jamjet.approval.timeout30mMax wait time before timeout (supports s, m, h)
spring.jamjet.approval.default-decisionrejectedDecision applied on timeout: approved or rejected

Triggering approval

To mark a specific request as requiring approval, set the jamjet.approval.required context key:

String result = chatClient.prompt("Transfer $50,000 to account 9876")
        .advisors(approvalAdvisor)  // or auto-injected
        .context("jamjet.approval.required", true)
        .call()
        .content();
// Thread blocks here until approval is received (or timeout)

The approval flow

sequenceDiagram
    participant User as User/Client
    participant App as Spring App
    participant Advisor as ApprovalAdvisor
    participant Registry as ApprovalWaitRegistry
    participant Controller as /jamjet/approvals
    participant Reviewer as Human Reviewer

    User->>App: chatClient.prompt("Transfer $50K")
    App->>Advisor: before() — REQUIRES_APPROVAL=true
    Advisor->>Registry: waitForApproval(executionId, 30m)
    Note over Advisor,Registry: Thread blocks (durable pause)

    Note over Controller,Reviewer: Meanwhile, reviewer is notified
    Reviewer->>Controller: POST /jamjet/approvals/{executionId}
    Note right of Reviewer: {"decision":"approved","user_id":"jane"}
    Controller->>Registry: complete(executionId, decision)
    Registry-->>Advisor: ApprovalDecision(approved)

    Note over Advisor: Approval received — continue
    Advisor->>App: proceed with ChatClient call
    App-->>User: "Transfer initiated..."

Approving or rejecting via REST

When approval is enabled, the auto-configuration registers JamjetApprovalController with two endpoints:

Approve an execution:

curl -X POST http://localhost:8080/jamjet/approvals/{executionId} \
  -H "Content-Type: application/json" \
  -d '{
    "decision": "approved",
    "user_id": "jane.doe",
    "comment": "Reviewed and approved"
  }'

Reject an execution:

curl -X POST http://localhost:8080/jamjet/approvals/{executionId} \
  -H "Content-Type: application/json" \
  -d '{
    "decision": "rejected",
    "user_id": "jane.doe",
    "comment": "Amount exceeds policy limit"
  }'

List pending approvals:

curl http://localhost:8080/jamjet/approvals/pending

When a rejection is received, the advisor throws an ApprovalRejectedException with the execution ID and the reviewer's comment.

The approval request body supports the following fields:

FieldTypeRequiredDescription
decisionStringYes"approved" or "rejected"
user_idStringNoIdentifier of the reviewer
commentStringNoHuman-readable justification
node_idStringNoSpecific node to approve (advanced)
state_patchMap<String, Object>NoState modifications to apply on approval

For more on human-in-the-loop patterns in agent systems, see Agentic AI Patterns.


Testing

AI agents are notoriously difficult to test. LLMs are non-deterministic --- the same prompt can produce different outputs on every call. Token costs add up when running tests against live APIs. And it is hard to assert on outputs that change every time.

JamJet's test module solves this with two complementary approaches: replay testing (replay a real execution without calling the LLM) and deterministic stubs (replace the LLM with a pattern-matched fake).

Add the test dependency

<dependency>
    <groupId>dev.jamjet</groupId>
    <artifactId>jamjet-spring-boot-starter-test</artifactId>
    <version>0.1.0</version>
    <scope>test</scope>
</dependency>

Replay testing with @WithJamjetRuntime and @ReplayExecution

Replay testing captures a production execution and replays it in your test suite. The test connects to a JamJet runtime (via Testcontainers), fetches the execution's event log, and lets you assert on the results --- without making any LLM calls.

import dev.jamjet.spring.test.annotations.WithJamjetRuntime;
import dev.jamjet.spring.test.annotations.ReplayExecution;
import dev.jamjet.spring.test.RecordedExecution;
import dev.jamjet.spring.test.AgentAssertions;
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;

@WithJamjetRuntime
class PortfolioAgentTest {

    @Test
    @ReplayExecution("exec-abc123")
    void agentProducesConsistentOutput(RecordedExecution execution) {
        AgentAssertions.assertThat(execution)
                .completedSuccessfully()
                .usedTool("WebSearch")
                .completedWithin(30, TimeUnit.SECONDS)
                .costLessThan(0.50);
    }

    @Test
    @ReplayExecution(value = "exec-abc123", forkAtNode = "retrieve")
    void forkAndRerunFromRetrieveStep(RecordedExecution execution) {
        AgentAssertions.assertThat(execution)
                .nodeCompleted("retrieve")
                .outputContains("portfolio");
    }
}

@WithJamjetRuntime is a JUnit 5 extension that starts a JamJet runtime container before your tests. In 0.1.0 its default image is ghcr.io/jamjet-labs/jamjet:0.3.1, which refuses anonymous pulls, so set the public image:

@WithJamjetRuntime(image = "jamjetdev/jamjet", tag = "0.3.1")

The 0.1.0 test container expects the runtime on port 7700. jamjetdev/jamjet:0.3.1 listens there; 0.4.0 and later listen on 8080.

@ReplayExecution specifies which execution to replay. The optional forkAtNode parameter lets you fork the execution at a specific node --- useful for testing "what if we changed the output of step X?"

The RecordedExecution record

RecordedExecution captures everything about a replayed execution:

FieldTypeDescription
executionIdStringUnique execution ID
workflowIdStringWorkflow ID
statusStringFinal status (Completed, Failed, Cancelled)
inputObjectInput that started the execution
finalStateObjectFinal state after all nodes completed
eventsList<ExecutionEvent>Full event log
nodesList<NodeExecution>Per-node execution details
totalDurationDurationWall-clock duration
toolCallCountintTotal tool invocations
totalCostUsddoubleAggregated token cost

Each NodeExecution contains nodeId, kind, status, input, output, duration, and retryCount.

AgentAssertions fluent API

The AgentAssertions.assertThat(execution) entry point returns a fluent API designed specifically for agent testing:

AssertionDescription
.completedSuccessfully()Execution status is Completed
.failedWith(errorContaining)Status is Failed with matching error message
.wasCancelled()Status is Cancelled
.completedWithin(amount, unit)Wall-clock duration within limit
.costLessThan(usd)Total cost below threshold
.usedTool(toolName)Tool was invoked at least once
.usedToolTimes(toolName, n)Tool was invoked exactly n times
.didNotUseTool(toolName)Tool was never invoked
.toolCallCount(matcher)Hamcrest matcher on total tool call count
.nodeCompleted(nodeId)Specific node completed successfully
.nodeRetried(nodeId, times)Node was retried exactly times times
.nodeCount(matcher)Hamcrest matcher on node count
.outputContains(substring)Final output contains substring
.outputMatches(regex)Final output matches regex pattern
.outputSatisfies(consumer)Custom assertion lambda on output
.hasEvent(eventType)Event log contains event of given type
.eventCount(matcher)Hamcrest matcher on event count
.auditTrailContains(eventType)Alias for .hasEvent()
.auditTrailSize(matcher)Alias for .eventCount()

All assertions are chainable --- .completedSuccessfully().usedTool("X").costLessThan(1.0) reads naturally and fails with clear error messages.

Deterministic model stubs

For unit tests where you do not want to replay a real execution, DeterministicModelStub lets you replace the ChatModel with a pattern-matched fake:

import dev.jamjet.spring.test.DeterministicModelStub;

var stub = DeterministicModelStub.builder()
        .onPromptContaining("weather", "Sunny, 72F in San Francisco")
        .onPromptContaining("stock price", "ACME: $142.50, up 2.3%")
        .defaultResponse("I don't have information about that topic.")
        .build();

// Use as a ChatModel in your Spring context
@Bean
ChatModel chatModel() {
    return stub;
}

The stub matches prompts in order: the first onPromptContaining pattern that matches wins. If no pattern matches, it returns the defaultResponse. The stub also records all calls so you can verify invocation counts:

assertEquals(3, stub.getCallCount());
assertEquals("weather in SF", stub.getCalls().get(0).getContents());
stub.reset(); // clear call history

DeterministicModelStub implements ChatModel, so it works with both call() and stream() --- the stream variant returns a single-element Flux with the matched response.


Observability

Micrometer metrics

When Spring Boot Actuator and Micrometer are on the classpath, JamjetMicrometerBridge automatically publishes execution metrics. This is enabled by default --- no opt-in required.

spring:
  jamjet:
    observability:
      micrometer: true           # default: true
      metric-prefix: jamjet      # default: jamjet

Metrics reference

MetricTypeTagsDescription
jamjet.execution.durationTimerstatusDuration of each execution
jamjet.execution.countCounterstatusTotal executions by status
jamjet.node.durationTimernode_id, node_kindDuration of each node
jamjet.node.retriesCounternode_idRetry count per node
jamjet.tool.callsCountertool_nameTool invocations by name
jamjet.tool.durationTimertool_nameDuration per tool call
jamjet.execution.cost.usdDistributionSummary---Token cost per execution
jamjet.audit.eventsCounterevent_typeAudit events by type

The metric prefix is configurable. If you set metric-prefix: myapp.agent, metrics become myapp.agent.execution.duration, etc.

Alert recommendations

AlertConditionWhy
High failure raterate(jamjet.execution.count{status="Failed"}) > 0.05 * rate(jamjet.execution.count)More than 5% of executions failing
Slow executionsjamjet.execution.duration{quantile="0.95"} > 30sP95 latency above 30 seconds
Cost spikerate(jamjet.execution.cost.usd) > 10Spending more than $10/minute
Excessive retriesrate(jamjet.node.retries) > 5Nodes retrying too often (flaky tools or rate limits)
Approval backlogPending approvals count growingReviewers not responding (webhook integration issue)

OpenTelemetry tracing

For distributed tracing, enable the OpenTelemetry bridge:

spring:
  jamjet:
    observability:
      opentelemetry: true        # default: false (opt-in)

This requires io.opentelemetry:opentelemetry-api on your classpath. The bridge creates a span hierarchy that mirrors the execution structure:

graph TD
    A["jamjet.execution<br/>executionId, workflowId"] --> B["jamjet.node.retrieve<br/>nodeId, nodeKind"]
    A --> C["jamjet.node.synthesize<br/>nodeId, nodeKind"]
    B --> D["jamjet.tool.WebSearch<br/>toolName"]
    C --> E["jamjet.model.call<br/>modelName"]

Each span carries JamJet-specific attributes:

SpanKindAttributes
jamjet.executionINTERNALjamjet.execution.id, jamjet.workflow.id
jamjet.node.{id}INTERNALjamjet.node.id, jamjet.node.kind
jamjet.tool.{name}CLIENTjamjet.tool.name
jamjet.model.callCLIENTjamjet.model.name

Error spans include StatusCode.ERROR, the exception message, and a recorded exception event --- standard OTel semantics that work with any tracing backend (Jaeger, Zipkin, Grafana Tempo, Datadog).

Completed spans carry jamjet.cost.usd when cost data is available, allowing you to correlate cost with latency in your tracing UI.


Complete example

Here is a full Spring Boot application that ties together durability, audit, approval, and observability:

pom.xml (dependencies)

<dependencies>
    <!-- Spring AI + OpenAI -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <!-- JamJet durability -->
    <dependency>
        <groupId>dev.jamjet</groupId>
        <artifactId>jamjet-spring-boot-starter</artifactId>
        <version>0.1.0</version>
    </dependency>

    <!-- Spring Boot Actuator (enables Micrometer metrics) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Test -->
    <dependency>
        <groupId>dev.jamjet</groupId>
        <artifactId>jamjet-spring-boot-starter-test</artifactId>
        <version>0.1.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

application.yml

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}

  jamjet:
    runtime-url: http://localhost:7700
    durability-enabled: true

    audit:
      enabled: true
      include-prompts: true
      include-responses: true

    approval:
      enabled: true
      timeout: 15m
      default-decision: rejected

    observability:
      micrometer: true
      metric-prefix: jamjet

DurableAgentApplication.java

import dev.jamjet.spring.advisor.JamjetApprovalAdvisor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.*;

@SpringBootApplication
public class DurableAgentApplication {

    public static void main(String[] args) {
        SpringApplication.run(DurableAgentApplication.class, args);
    }

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.build(); // Durability + audit advisors auto-injected
    }

    @RestController
    @RequestMapping("/api/agent")
    static class AgentController {

        private final ChatClient chatClient;

        AgentController(ChatClient chatClient) {
            this.chatClient = chatClient;
        }

        // Standard durable call — crash recovery + audit trail
        @PostMapping("/ask")
        String ask(@RequestBody String prompt) {
            return chatClient.prompt(prompt)
                    .call()
                    .content();
        }

        // High-stakes call — requires human approval before proceeding
        @PostMapping("/ask-with-approval")
        String askWithApproval(@RequestBody String prompt) {
            return chatClient.prompt(prompt)
                    .context(JamjetApprovalAdvisor.REQUIRES_APPROVAL_KEY, true)
                    .call()
                    .content();
        }
    }
}

Running it

# Terminal 1: Start JamJet runtime (add --platform linux/amd64 on Apple silicon)
docker run -p 7700:8080 -e JAMJET_DEV_MODE=1 jamjetdev/jamjet:0.5.0

# Terminal 2: Start the Spring Boot app
export OPENAI_API_KEY=sk-...
mvn spring-boot:run

# Terminal 3: Make a durable call
curl -X POST http://localhost:8080/api/agent/ask \
  -H "Content-Type: text/plain" \
  -d "What are the top 3 AI trends in 2026?"

# Make a call that requires approval
curl -X POST http://localhost:8080/api/agent/ask-with-approval \
  -H "Content-Type: text/plain" \
  -d "Draft an email to all customers about a pricing change"

# In another terminal: approve the pending execution
curl http://localhost:8080/jamjet/approvals/pending
# Copy the executionId from the response, then:
curl -X POST http://localhost:8080/jamjet/approvals/{executionId} \
  -H "Content-Type: application/json" \
  -d '{"decision":"approved","user_id":"admin","comment":"Looks good"}'

For chat memory, see Engram — JamJet's temporal-knowledge-graph memory layer with first-class Spring Boot integration via engram-spring-boot-starter.


Requirements

RequirementMinimum version
Java21+
Spring Boot3.4+
Spring AI1.0+
JamJet runtime0.3.1+ (Docker or binary)

Next steps

  • LangChain4j Integration: use JamJet as a durable execution layer for LangChain4j agents with JamjetDurableAgent and JamjetChatMemoryStore
  • Java SDK Reference: full API coverage for tools, strategies, IR compilation, and the runtime client
  • Java Quickstart: build your first agent and workflow from scratch with the Java SDK
  • Core Concepts: agents, nodes, state, and durability in depth
  • Agentic AI Patterns --- strategy selection, tool design, and production patterns for agent systems

On this page