License
Specification: Jakarta Agentic AI Specification Version: 1.0 Status: Draft Release: July 30, 2026
Copyright
Copyright (c) 2026 Eclipse Foundation AISBL.
Eclipse Foundation Specification License - v2.0
This license (the “License”) governs the use, modification and distribution of a specification generated pursuant to the Eclipse Foundation Specification Process.
By using the file or document that incorporates this License (the “Document”), you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions.
Permission to copy and distribute the contents of the Document, in any medium for any purpose, and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the Document that you copy or distribute:
-
link or URL to the Document
-
All existing copyright notices, or if one does not exist, a notice (hypertext is preferred, but a textual representation is permitted) of the form: “Copyright © [date-of-document] Eclipse Foundation AISBL [URL to this license] This document includes material copied from [title and URL of the Eclipse Foundation specification document].”
In addition to the license granted above, you are granted the right to create modifications or derivatives of the Document:
-
in software that implements the Document, in supporting materials accompanying such software, and in documentation of such software (collectively the “Works”), such Works to be made available under terms of your choice PROVIDED that all such Works include a notice of the form: “This [software/document (choose as applicable)] includes material derived from [title and URL of the Eclipse Foundation specification document].”; and
-
in materials that demonstrate or describe conformance with the Document PROVIDED that all such materials include a notice of the form: “This material demonstrates conformance with the [title and URL of the Eclipse Foundation specification document].”
NOTWITHSTANDING THE FOREGOING, the creation or publication of derivative works of the Document or portions of the Document for use as a specification or standard is expressly prohibited.
Disclaimers
THE DOCUMENT IS PROVIDED "AS IS," AND TO THE EXTENT PERMITTED BY APPLICABLE LAW THE COPYRIGHT HOLDERS AND THE ECLIPSE FOUNDATION AISBL MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE CONTENTS OF THE DOCUMENT ARE SUITABLE FOR ANY PURPOSE; NOR THAT THE IMPLEMENTATION OF SUCH CONTENTS WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS.
TO THE EXTENT PERMITTED BY APPLICABLE LAW THE COPYRIGHT HOLDERS AND THE ECLIPSE FOUNDATION AISBL WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE DOCUMENT OR THE PERFORMANCE OR IMPLEMENTATION OF THE CONTENTS THEREOF.
The name and trademarks of the copyright holders or the Eclipse Foundation AISBL may NOT be used in advertising or publicity pertaining to this document or its contents without specific, written prior permission.
1. Introduction
Jakarta Agentic AI provides vendor-neutral APIs for building, deploying, and running AI agents on Jakarta EE runtimes. The specification standardizes agent lifecycles, workflow composition, and integration with foundational AI technologies such as Large Language Models.
1.1. Scope
-
Defines common usage patterns and lifecycles for AI agents on Jakarta EE runtimes.
-
Provides a minimal facade for foundational AI capabilities (e.g., LLMs), with pluggable access to existing APIs.
-
A mechanism to define dynamic agent workflows using a fluent Java API.
-
Integrates with other Jakarta EE APIs (Validation, RESTful Web Services, JSON Binding, Persistence, Data, Transactions, NoSQL, Concurrency, Security, Messaging).
-
Aims to utilize Jakarta Config and allows implementations to use MicroProfile Config.
-
Implementations may provide OpenTelemetry integration.
-
Designed for Jakarta EE runtimes, but potentially usable in Quarkus, Micronaut, and Spring Boot.
The current version of Jakarta Agentic AI targets Java SE 17 or higher and Jakarta EE Platform 10 or higher. See the Bibliography for referenced specifications.
1.2. Motivation
Modern enterprise applications increasingly require automation capabilities that:
-
Monitor systems and respond to events
-
Make context-aware decisions using AI/ML models
-
Execute dynamic workflows based on LLM-generated insights
-
Integrate with existing enterprise infrastructure
While AI and LLM technologies have rapidly matured, there is no standard way to build AI agents in Jakarta EE applications. Developers must rely on vendor-specific frameworks or integrate multiple disparate technologies themselves, leading to:
-
Lack of portability between Jakarta EE implementations
-
Inconsistent programming models
-
Difficulty integrating with CDI and other Jakarta EE technologies
-
Custom boilerplate code for agent orchestration, even for common patterns
Jakarta Agentic AI addresses these challenges by providing a standard API that feels natural to Java and Jakarta EE developers, similar to how Jakarta Servlet, Jakarta RESTful Web Services, and Jakarta Batch standardized their respective domains.
1.3. Goals
The primary goals of Jakarta Agentic AI are:
- Annotation-driven agent development
-
Define agent workflows using familiar Jakarta EE annotations, marking methods as triggers, decisions, actions, outcomes, etc.
- CDI integration
-
Agents are CDI beans with full support for dependency injection, interceptors, scoping, etc.
- Simple LLM abstraction
-
Provide a lightweight facade for accessing Large Language Models without standardizing the LLM APIs themselves. Vendors plug in different LLM backends while applications enjoy a consistent basic interface.
- Workflow orchestration
-
Enable declarative and programmatic workflows where the runtime manages execution flow between agent lifecycle phases.
- Vendor neutrality
-
Allow implementations across Jakarta EE compatible products and other CDI/Java-based runtimes.
- Jakarta EE alignment
-
Follow familiar Jakarta EE patterns and conventions, ensuring consistency with specifications like Jakarta Persistence, Jakarta RESTful Web Services, and Jakarta Batch.
1.4. Non-goals
The following are explicitly outside the scope of this specification:
- Standardizing LLM APIs
-
Jakarta Agentic AI provides a simple facade for accessing LLMs but does not standardize the underlying LLM APIs. LLM provider APIs vary significantly and evolve rapidly, making standardization impractical at the current moment. The facade provides ready access to the underlying provider APIs through
unwrap(). A separate Jakarta EE LLM API may be appropriate in the future. - Machine learning frameworks
-
This specification focuses on agent orchestration and lifecycle management, not ML framework standardization.
- Training and fine-tuning
-
Model training, fine-tuning, and ML model lifecycle management are outside the scope of agent orchestration.
1.5. How this Specification is Organized
The chapters that follow take the reader from concept to normative behavior to reference material:
-
Programming Model introduces the agent and workflow model and shows a complete example.
-
Annotations and Lifecycle is the normative chapter that defines
@Agent,@Trigger,@Decision,@Action,@Outcome, and@HandleException, along with parameter injection, validation, error handling, and workflow termination. -
CDI Integration covers scopes, the
@WorkflowScopedcustom scope, event observation rules, interceptors, and lifecycle callbacks. -
Large Language Model Integration defines the
LargeLanguageModelfacade,LLMException, and usage guidance. -
Examples illustrates common agent patterns end to end.
2. Programming Model
This chapter presents the Jakarta Agentic AI programming model: what an agent is, how a workflow is composed, and how the pieces fit together. The Annotations and Lifecycle chapter then defines each annotation normatively.
2.1. Agents and Workflows
An agent is a CDI bean annotated with @Agent that orchestrates a flexible
workflow. Every workflow begins with a trigger and may include any
combination of the following phases:
-
Trigger: detects an event or condition that initiates the workflow.
-
Decisions: analyze workflow state and determine whether and how to proceed.
-
Actions: execute operations as part of the workflow.
-
Outcome: produces or finalizes the result of the workflow.
Decisions and actions can be intermixed in any sequence, allowing agents to implement simple linear workflows or complex conditional branching.
In addition, an agent may declare @HandleException methods to recover from
errors that occur during workflow execution.
2.1.1. Workflow Execution
The runtime orchestrates annotated methods using a sequential execution model:
-
The trigger fires.
-
Decisions and actions execute in order, intermixed as defined.
-
Decisions control flow: a positive return continues, negative terminates.
-
The outcome executes once the workflow completes without early termination.
-
Exception handlers execute if exceptions occur in any phase.
The phase rules and per-annotation cardinality are defined normatively in the Annotations and Lifecycle chapter.
2.1.2. Future Direction
A programmatic workflow API is planned for a subsequent release. It will
wire annotated methods (the basic building blocks) into dynamic, potentially
complex control flows that may be modified at runtime — enabling dynamic
decision routing, conditional action execution, and workflows that adapt
based on agent state or LLM interactions. An initial workflow could be
defined or loaded from storage in a @PostConstruct method and persisted in
a @PreDestroy method.
2.2. An Agent at a Glance
The following agent illustrates the basic model: a CDI bean with a trigger, a decision that consults an LLM, and an action.
@Agent
public class MonitoringAgent {
@Inject
private LargeLanguageModel llm;
@Inject
private EventLogger logger;
@Trigger
public void onSystemAlert(Alert alert) {
// Triggered by an external CDI event
}
@Decision
public boolean shouldEscalate(Alert alert) {
String analysis = llm.query("Should this alert be escalated?", alert);
return analysis.toLowerCase().contains("yes");
}
@Action
public void escalateAlert(Alert alert) {
logger.logEscalation(alert);
}
}
2.3. Jakarta EE Integration
Agents are first-class Jakarta EE citizens. They inject other beans, are managed by CDI, and participate in transactions, validation, persistence, messaging, concurrency, security, and REST integration just like any other Jakarta EE component.
- Validation
-
-
Jakarta Validation — constraint annotations on agent life-cycle method parameters; see Jakarta Validation Constraints.
-
- Data access
-
-
Jakarta Persistence — relational databases
-
Jakarta NoSQL — non-relational databases
-
Jakarta Data — repositories
-
- Transactional operations
-
-
Jakarta Transactions —
@Transactionalagent methods
-
- Data serialization
-
-
Jakarta JSON Binding — used by
LargeLanguageModelfor structured prompt parameters and typed responses; see Large Language Model Integration.
-
- Messaging and integration
-
-
Jakarta Messaging — publish and consume messages (a future trigger source)
-
- REST integration
-
-
Jakarta RESTful Web Services — expose agents as REST resources or consume external services; a REST
POSTendpoint is an anticipated future trigger source
-
- Asynchronous operations
-
-
Jakarta Concurrency — managed executors for asynchronous work within agent logic
-
@Agent
public class DataIntegratedAgent {
@Inject private EntityManager entityManager; // Jakarta Persistence
@Inject private CustomerRepository customerRepo; // Jakarta Data
@Inject private Template mongoTemplate; // Jakarta NoSQL
@Inject private ManagedExecutorService executor; // Jakarta Concurrency
@Action
@Transactional // Jakarta Transactions
public void processCustomerData(
@Valid @NotNull CustomerId id) { // Jakarta Validation
Customer customer = entityManager.find(Customer.class, id);
List<Order> orders = customerRepo.findOrdersByCustomerId(id);
mongoTemplate.insert(new AnalyticsEvent(customer, orders));
executor.submit(() -> generateReport(customer, orders));
}
}
For CDI scoping, observer rules, interceptors, and lifecycle callbacks, see the CDI Integration chapter. For the LLM facade, see the Large Language Model Integration chapter.
3. Annotations and Lifecycle
This chapter is the normative reference for the annotations included in the specification, including the lifecycle annotations. Each lifecycle annotation marks a method as participating in a specific phase of the agent workflow. The chapter also defines parameter resolution, validation, error handling, return type handling, and workflow termination.
3.1. Workflow Phases and Annotations
Annotations define agent behavior. They are structured per agent class, generally in order:
-
@Agent— required, exactly one (class-level) -
@Trigger— required, exactly one (may be relaxed in the future) -
@Decision— optional, zero or more -
@Action— optional, zero or more -
@Outcome— optional, zero or one (may be relaxed in the future) -
@HandleException— optional, zero or more
3.2. @Agent
The @Agent annotation marks a class as an AI agent. Agent classes are
CDI beans and follow CDI bean definition rules.
3.2.1. Semantics
-
Required: every agent class must be annotated with
@Agent. -
CDI bean: the annotated class becomes a CDI managed bean.
-
Discovery: implementations must discover all
@Agent-annotated classes during deployment. -
Default scope: if no scope is specified,
@WorkflowScopedis the default. Agents can also be@ApplicationScoped. See CDI Integration for scope semantics and bean requirements. -
Naming: the agent name defaults to the simple class name with the first character converted to lower case when omitted.
@Agent
@ApplicationScoped
public class MonitoringAgent { /* longer-lived agent */ }
@Agent
@WorkflowScoped
public class WorkflowAgent { /* per-workflow lifecycle */ }
@Agent
public class DefaultScopedAgent { /* @WorkflowScoped is implied */ }
3.2.2. Workflow Context
Regardless of the scope, the runtime maintains a fresh workflow context for
each workflow invocation. For @WorkflowScoped agents, that context is
tied to the workflow itself. For @ApplicationScoped agents, the agent
instance may be reused across multiple workflows, but each invocation still
has an isolated workflow context that begins with a trigger and typically
ends with an outcome.
LargeLanguageModel conversational state follows the same boundaries:
it is bound to the workflow context and remains isolated per workflow
even when the agent is @ApplicationScoped. See
Conversational State and Threading.
3.3. @Trigger
The @Trigger annotation marks the entry point of an agent workflow.
3.3.1. Cardinality
Currently there must be exactly one @Trigger method per agent class. This
constraint will be relaxed in future releases.
3.3.2. Invocation
In the initial release, triggers are invoked by CDI events. A trigger
method may use @Observes on the event parameter to explicitly declare the
observed type. However, explicitly using @Observes is not required. There
must be exactly one event parameter. The triggering event becomes part of the
workflow state and is available for
parameter resolution in later phases.
In future releases, additional trigger sources are anticipated, such as
Jakarta Messaging, manual invocation through a lifecycle API, or REST POST
requests.
| CDI observer behavior depends on agent scope. See the Event Observation Rules in the CDI Integration chapter. |
3.3.3. Parameters
Trigger methods follow the standard parameter resolution rules. The required triggering event is the most common parameter.
3.3.4. Return Type
Trigger methods support two return patterns:
-
Void — initialization with side effects only; no data is passed forward.
-
Domain object — the returned object becomes part of workflow state and is available for injection into later phases.
@Agent
public class EventDrivenAgent {
@Trigger
public void onCustomEvent(CustomEvent event) {
logger.info("Workflow triggered for event: " + event.getId());
}
}
@Agent
public class AnalyzingAgent {
@Trigger
public EventAnalysis analyzeEvent(CustomEvent event, LargeLanguageModel llm) {
String analysis = llm.query("Classify this event", event);
EventAnalysis result = new EventAnalysis();
result.setEvent(event);
result.setClassification(analysis);
return result;
}
}
3.4. Parameter Resolution
All workflow methods (annotated with @Trigger, @Decision, @Action,
@Outcome, and @HandleException) receive their parameters through automatic
resolution based on type and the current workflow state. Each annotation uses
this single set of rules.
3.4.1. Resolution Order
When a workflow method is invoked, the runtime resolves each parameter in the following order:
-
Trigger event — if the parameter type matches the triggering event.
-
Previous phase results — if the parameter type matches an object returned by an earlier step in the current workflow. When multiple steps have returned objects of the same type, the most recently returned object is used.
-
LargeLanguageModel— if the parameter type isLargeLanguageModel. -
Exception — for
@HandleExceptionmethods only, the thrown exception (or any supertype thereof) is the required first parameter. -
CDI injection — if the parameter is a CDI bean.
3.4.2. Jakarta Validation Constraints
Parameters may declare Jakarta Validation constraints (@Valid, @NotNull,
@NotEmpty, @Size, etc). Validation occurs before parameter injection and
workflow method invocation. If validation fails, a
ConstraintViolationException is raised; it can be caught by an
@HandleException method or propagated to the container.
ConstraintViolationException is a runtime exception, so explicit handling
is not required. Common scenarios include parameter constraint failures,
custom constraint violations, and validation of complex domain objects passed
between phases.
3.5. @Decision
The @Decision annotation marks a method that decides whether and how the
workflow should proceed.
3.5.2. Parameters
Decision methods follow the standard parameter resolution rules.
3.5.3. Return Type
Decisions support three return patterns:
- Boolean
-
-
true— proceed with the workflow. -
false— terminate the workflow.
-
Resultrecord-
Standardizes a decision outcome with a boolean success flag and optional details object:
public record Result(boolean success, Object details) {}-
success == trueproceeds;success == falseterminates. -
details(if non-null) becomes part of workflow state and is available for injection by its runtime type into later steps.
-
- Domain object
-
-
Non-
null— proceed; the returned object becomes part of workflow state. -
null— terminate the workflow.
-
@Agent
public class DecisionExamples {
@Inject
private LargeLanguageModel llm;
// Boolean
@Decision
public boolean shouldProcess(Event event) {
String analysis = llm.query("Should this event be processed?", event);
return analysis.toLowerCase().contains("yes");
}
// Result record
@Decision
public Result evaluateEvent(Event event) {
String analysis = llm.query("Analyze this event", event);
boolean proceed = requiresAction(analysis);
AnalysisDetails details = proceed ? parseDetails(analysis) : null;
return new Result(proceed, details);
}
// Domain object
@Decision
public AnalysisResult analyzeEvent(Event event) {
String analysis = llm.query("Analyze this event", event);
return requiresAction(analysis) ? new AnalysisResult(analysis) : null;
}
}
3.6. @Action
The @Action annotation marks a method that performs work as part of the
workflow.
3.6.1. Cardinality
An agent may define zero or more @Action methods. All actions complete
before the outcome phase runs.
3.6.2. Parameters
Action methods follow the standard parameter resolution rules.
3.6.3. Return Type
Actions support two return patterns:
-
Void — side effects only (e.g., sending alerts, updating databases).
-
Domain object — the returned object becomes part of workflow state and is available for injection into later phases.
@Agent
public class ActionExamples {
// Side effects
@Action
public void handleFraud(Fraud fraud, BankTransaction transaction) {
if (fraud.isSerious()) {
alertBankSecurity(fraud);
}
Customer customer = getCustomer(transaction);
alertCustomer(fraud, transaction, customer);
}
// Pass result forward
@Action
public FraudReport processFraudCase(Fraud fraud, BankTransaction transaction) {
FraudReport report = new FraudReport();
report.setFraudType(fraud.getType());
report.setTransaction(transaction);
report.setTimestamp(System.currentTimeMillis());
persistReport(report);
return report;
}
@Outcome
public void recordOutcome(FraudReport report) {
auditLog("Fraud case processed: " + report.getId());
}
}
You can apply or use Jakarta Transactions in lifecycle methods including Actions:
@Agent
public class TransactionalAgent {
@Action
@Transactional
public void updateRecords(UpdatePlan plan) {
database.updateRecords(plan.getRecords());
}
}
3.7. Execution Order
The execution order of @Action and @Decision methods can be intermixed
as needed. The runtime determines their order using the following rules,
in decreasing precedence:
-
@Priorityon the method —jakarta.annotation.Priorityvalue is the sort key; lower values execute first. -
orderattribute on the@Actionand@Decisionannotations — used as the sort key when@Priorityis absent; lower values execute first. -
Source declaration order — fallback when no
@Actionor@Decisionmethod in the agent declares@Priorityor an explicitorder.
Java SE does not guarantee that reflection returns methods
in source declaration order. However, all major implementations do so in
practice. Portable applications requiring strict ordering must use
@Priority or order.
|
3.8. @Outcome
The @Outcome annotation marks a method that finalizes the workflow. It
produces the final workflow result, runs completion logic, and marks the
end of successful workflow execution.
3.8.1. Cardinality
Currently, an agent may declare zero or one @Outcome method. This
constraint may be relaxed in a future release.
3.8.2. Parameters
Outcome methods follow the standard parameter resolution rules. Any object produced by an earlier phase is eligible for injection.
3.9. @HandleException
The @HandleException annotation marks a method that handles exceptions
occurring during workflow execution. For handler matching and dispatch
semantics, see Error Handling.
3.9.2. Parameters
Exception handler methods follow the standard parameter resolution rules. The thrown exception (or any supertype) is the required first parameter.
3.9.3. Return Type
Exception handler methods must return void. They are intended for error
recovery, logging, and cleanup.
3.9.4. Examples
@Agent
public class ExceptionHandlingExamples {
// Recoverable - returns normally, workflow continues
@HandleException
public void handleRecoverable(IOException ex, BankTransaction transaction) {
logger.warn("I/O error, retrying: " + transaction.getId(), ex);
retryQueue.add(transaction);
}
// Fatal - re-throws, workflow stops
@HandleException
public void handleFatal(SecurityException ex, BankTransaction transaction) {
logger.error("Security violation", ex);
auditService.logSecurityBreach(transaction);
throw ex;
}
// Conditional recovery
@HandleException
public void handleWithFallback(Exception ex, BankTransaction transaction) {
if (isRecoverable(ex)) {
performRecovery(transaction);
} else {
throw new WorkflowFailureException("Unrecoverable", ex);
}
}
}
3.10. Error Handling
When an exception occurs in any workflow phase (@Trigger, @Decision,
@Action, or @Outcome), the runtime searches for a matching
@HandleException method. The most specific exception type match wins,
following Java’s exception hierarchy.
-
If the handler returns normally, the workflow continues with the next step.
-
If the handler re-throws or throws a new exception, the workflow stops and the exception propagates to the container.
-
If no handler is defined, the exception propagates to the container.
-
Exceptions thrown from handler methods are not handled recursively.
@Agent
public class ResilientAgent {
@Trigger
public void onEvent(Event event) {
if (event == null) {
throw new IllegalArgumentException("Event cannot be null");
}
}
@Decision
public boolean analyze(Event event) {
return analyzeEvent(event); // may throw IOException
}
@HandleException
public void handleIOException(IOException e, Event event) {
logger.warn("I/O error, will retry", e);
scheduleRetry(event);
}
@HandleException
public void handleGenericException(Exception e, Event event) {
logger.error("Unexpected error", e);
logFailure(event, e);
}
}
3.11. Workflow Termination
A workflow terminates when any of the following occurs:
-
Normal completion — the outcome phase completes successfully (or, if no outcome is defined, the last phase completes).
-
Decision stop — a decision phase returns a negative result.
-
Unhandled exception — an exception is raised and no matching handler is defined.
-
Handler exception — an exception handler itself throws.
Any runtime-managed state associated with the current workflow is discarded once the workflow terminates.
3.11.1. One Phase per Method
Each lifecycle annotation marks a method as participating in a single
workflow phase. A method must not declare more than one of @Trigger,
@Decision, @Action, @Outcome, or @HandleException; combining two or
more of them on the same method is a deployment error.
3.11.2. Inherited Methods
Phase methods declared on a superclass are inherited by an agent subclass and participate in the workflow as if declared on the subclass itself. A method that overrides an inherited phase method participates only when the overriding method is itself annotated; following Java semantics, phase annotations are not implicitly carried onto an unannotated override.
3.12. Workflow Patterns
Agents are not constrained to a rigid template — only @Trigger is
required. The following patterns illustrate some common shapes.
3.12.1. Trigger + Action
@Agent
public class SimpleNotificationAgent {
@Trigger
public void onAlert(SystemAlert alert) { }
@Action
public void sendNotification(SystemAlert alert) {
emailService.notifyAdmins(alert);
}
}
3.12.2. Sequential Phases
@Agent
public class DataProcessingAgent {
@Trigger
public void onDataReceived(DataBatch data) { }
@Action public void validateData(DataBatch data) { validator.check(data); }
@Action public void transformData(DataBatch data) { transformer.process(data); }
@Outcome public void persistData(DataBatch data) { database.save(data); }
}
3.12.3. Intermixed Decisions and Actions
@Agent
public class OrderAgent {
@Trigger
public void onOrder(Order order) { }
@Decision
public boolean isLegitimate(Order order, LargeLanguageModel llm) {
return llm.query("Does this order appear legitimate?", order).contains("yes");
}
@Decision
public boolean inStock(Order order) {
return inventory.available(order);
}
@Action
public void reserveStock(Order order) {
inventory.reserve(order);
}
@Decision
public boolean paymentClears(Order order) {
return payment.charge(order);
}
@Action
public void shipOrder(Order order) {
shipping.dispatch(order);
}
@Outcome
public void confirmOrder(Order order) {
notifier.confirm(order);
}
}
4. CDI Integration
This chapter describes how Jakarta Agentic AI integrates with Jakarta
Contexts and Dependency Injection (CDI), including agent scoping, the
@WorkflowScoped custom scope, event observation rules, interceptors, and
lifecycle callbacks.
4.1. Agents as CDI Beans
Every agent is a CDI managed bean. This provides:
-
Dependency injection of other beans, Jakarta EE resources, and services
-
Interceptors for cross-cutting concerns
-
CDI events for production and observation
-
Lifecycle callbacks (
@PostConstruct,@PreDestroy) -
Flexible scoping
4.2. Agent Scopes
An agent uses one of two scopes:
-
@WorkflowScoped(default) — a new instance per workflow context. -
@ApplicationScoped— a single shared instance across workflows.
If no scope is declared, @WorkflowScoped applies.
4.2.1. @WorkflowScoped
@WorkflowScoped is a custom CDI scope that ties the agent’s lifecycle to
the workflow context. It is a normal scope (@NormalScope).
When an agent is @WorkflowScoped:
-
A new instance is created for each workflow context.
-
The instance is active from trigger invocation through workflow completion.
-
The instance is destroyed after the workflow completes or fails.
-
Each concurrent workflow has its own instance.
4.2.2. @ApplicationScoped
When an agent is @ApplicationScoped, a single instance is shared across
all workflows. Each invocation still runs in an isolated workflow context.
Because a single instance may handle multiple concurrent workflow
invocations, @ApplicationScoped agents must be thread-safe; the agent
itself must either be stateless or guard any shared state with
appropriate concurrency controls.
4.2.3. Threading and Concurrency
-
Sequential phases — within a single workflow context, annotated methods (
@Trigger,@Decision,@Action,@Outcome,@HandleException) execute sequentially (see the Execution Order rules in Annotations and Lifecycle). -
@WorkflowScopedagents — a workflow context typically executes on a single thread; the agent instance is therefore not exposed to concurrent invocation by the runtime. -
@ApplicationScopedagents — the same instance may serve concurrent workflows; the agent must be thread-safe (see above). -
LargeLanguageModel— implementations must be thread-safe within a single workflow context and must not leak conversational state across concurrent workflow contexts. See Conversational State and Threading for the full contract.
4.3. Event Observation Rules
Agents may produce CDI events freely. Event observation depends on agent scope:
-
@WorkflowScopedagents — may observe CDI events only through their@Triggermethod. General observer methods (@Observeswithout@Trigger) are not supported. -
@ApplicationScopedagents — may use both@Triggermethods and general CDI observer methods.
A trigger method may optionally use @Observes on the event parameter to
explicitly declare the observed type.
4.4. Interceptors
Standard CDI interceptors apply to agent methods. Jakarta Transactions is a common case:
@Agent
public class TransactionalAgent {
@Inject
private EntityManager entityManager;
@Action
@Transactional
public void updateDatabase(Data data) {
entityManager.persist(data);
}
}
4.5. Lifecycle Callbacks
Agents support standard CDI lifecycle callbacks:
@Agent
@WorkflowScoped
public class LifecycleAwareAgent {
private static final Logger logger =
Logger.getLogger(LifecycleAwareAgent.class.getName());
@Inject
private ResourcePool resources;
@PostConstruct
public void initialize() {
logger.info("Agent initialized");
resources.allocate();
}
@PreDestroy
public void cleanup() {
logger.info("Agent destroying");
resources.release();
}
}
4.6. LargeLanguageModel as a CDI Bean
The LargeLanguageModel interface is provided as a CDI bean by the
implementation and is injected into agents like any other bean. Its
conversational state lifecycle aligns with workflow context boundaries; see
Conversational State and Threading for the
full contract.
5. Large Language Model Integration
This chapter defines the LargeLanguageModel facade, the LLMException
runtime exception, and provides usage guidance grounded in the contract.
5.1. Why a Facade
Jakarta Agentic AI uses a facade pattern for LLM access. The
LargeLanguageModel interface provides a small, consistent portability
layer while allowing implementations to integrate any LLM backend.
- Abstraction
-
Applications depend on the facade, not on a specific LLM implementation.
- Vendor flexibility
-
Implementations can integrate any LLM provider or underlying SDK.
- Unwrapping
-
The
unwrap()method, defined in theLargeLanguageModelinterface, allows access to vendor-specific APIs when needed, following theEntityManager.unwrap()pattern from Jakarta Persistence. - Evolution
-
LLM technologies evolve rapidly; the facade insulates applications from those changes. The facade could be evolved into a separate specification in the future.
5.2. LargeLanguageModel Interface
The jakarta.ai.agent.LargeLanguageModel interface defines a focused set of
operations:
-
query(…)— an overloaded method that sends a prompt to the model. Overloads cover two independent choices: whether to receive the rawStringresponse or a value deserialized to a caller-specified type, and whether to supply positional parameters substituted into{}placeholders in the prompt (or, when no placeholder is present, attached as a single piece of structured context). Jakarta JSON Binding is used to serialize prompt parameters and deserialize typed responses, letting domain objects flow in and out without application-specific serialization code. -
unwrap(…)— return the underlying vendor-specific implementation so callers can reach provider APIs or features not exposed by the facade.
See the API Javadoc for the full method signatures, parameter semantics, and per-overload behavioral requirements.
Implementations are provided as CDI beans and injected into agents like any other bean; see LargeLanguageModel as a CDI Bean for the injection contract.
5.2.1. Conversational State and Threading
Implementations of the LargeLanguageModel interface maintain conversational state for the
current workflow context across calls to the query(…) method, so that successive
calls within a workflow can build on prior exchanges. The lifecycle of that
state follows the workflow context boundaries defined in
CDI Integration.
-
@WorkflowScopedagents — conversational state is bound to the workflow context and must end when the workflow context ends. -
@ApplicationScopedagents — an instance of theLargeLanguageModelinterface may be shared across concurrent workflows, but conversational state must remain isolated per workflow context and must not leak across concurrent invocations. -
Thread safety — implementations must be thread-safe within a single workflow context.
5.3. Exceptions
All query(…) methods may throw:
-
IllegalArgumentException— for null or invalid parameters, placeholder or parameter-count mismatches, or conversion failures (input parameters that cannot be converted). -
LLMException— for LLM service errors or conversion failures (responses that cannot be converted to the requested type). See below for details.
unwrap() throws an IllegalArgumentException if the underlying
implementation cannot be unwrapped to the requested type.
5.3.1. LLMException
The LLMException class is a runtime exception thrown when LLM operations fail. As a
runtime exception, it does not require explicit handling but may be caught
by methods annotated with the @HandleException annotation in agent workflows. Common failure scenarios include:
-
Communication failures with the LLM service
-
Invalid or malformed responses from the LLM (truncated output, error payloads, protocol-level failures)
-
Rate limiting or quota exceeded
-
Model unavailability or timeout
-
Response deserialization failures when a typed result is requested and the LLM response cannot be converted to the expected Java type
5.4. Usage Guidance
5.4.1. Prompt Construction
When using the varargs versions of the query(…) overloaded methods, use the exact token {} for
positional substitution.
-
When a prompt contains one or more
{}placeholders, the number of supplied parameters must exactly match the number of placeholders. -
When a prompt contains no placeholder, at most one supplied parameter may be provided as structured context.
@Decision
public boolean goodPrompt(Issue issue) {
String response = llm.query(
"""
Analyze this issue and respond with YES or NO:
Title: {}
Description: {}
Should this issue be escalated to senior engineers?
Respond with only: YES or NO
""",
issue.getTitle(),
issue.getDescription()
);
return response.trim().equalsIgnoreCase("YES");
}
@Decision
public String classifyEvent(MyEvent event) {
return llm.query("Classify this event", event); // structured context
}
5.4.2. Typed Responses
Prefer typed responses over manual string parsing. Implementations deserialize the LLM response from JSON to the requested type via Jakarta JSON Binding.
@Decision
public FraudAnalysis analyzeTransaction(BankTransaction transaction) {
String prompt = """
Analyze this transaction for fraud.
Respond with JSON matching this structure:
{
"isFraudulent": true/false,
"confidence": 0.0-1.0,
"reason": "explanation",
"riskLevel": "LOW|MEDIUM|HIGH|CRITICAL"
}
""";
try {
return llm.query(prompt, FraudAnalysis.class, transaction);
} catch (LLMException e) {
logger.warn("Failed to deserialize fraud analysis", e);
return FraudAnalysis.unknown();
}
}
public record FraudAnalysis(
boolean isFraudulent,
double confidence,
String reason,
RiskLevel riskLevel
) {
public static FraudAnalysis unknown() {
return new FraudAnalysis(false, 0.0, "Analysis failed", RiskLevel.UNKNOWN);
}
}
When string parsing is unavoidable, parse defensively:
@Decision
public Priority parsePriority(Issue issue) {
String response = llm.query(
"Assess priority (CRITICAL, HIGH, MEDIUM, LOW) for this issue",
issue
);
String normalized = response.trim().toUpperCase();
for (Priority p : Priority.values()) {
if (normalized.contains(p.name())) {
return p;
}
}
logger.warn("Could not parse priority from: " + response);
return Priority.MEDIUM;
}
5.4.3. Error Handling
Handle LLM failures inline with try-catch, or workflow-wide with
@HandleException. See Error Handling in
Annotations and Lifecycle for handler matching rules
and the behavior when a handler returns normally vs. re-throws.
@Decision
public boolean robustEvaluation(Task task) {
try {
String response = llm.query("Evaluate this task", task);
return parseResponse(response);
} catch (LLMException e) {
logger.warn("LLM service error, using fallback", e);
return task.getPriority() == Priority.HIGH;
} catch (IllegalArgumentException e) {
logger.error("Invalid parameter to LLM", e);
return false;
}
}
@HandleException
public void handleLLMError(LLMException e, Task task) {
logger.error("LLM failed for task: " + task.getId(), e);
task.setStatus(TaskStatus.NEEDS_MANUAL_REVIEW);
entityManager.merge(task);
// Workflow continues after this method returns
}
5.4.4. Vendor-Specific Access
Use the unwrap() method to reach vendor-specific features when portability is not
required:
@Agent
public class AdvancedAgent {
@Inject
private LargeLanguageModel llm;
@Decision
public AdvancedResult useVendorFeatures(Data data) {
String basicResult = llm.query("Basic analysis", data);
try {
SomeLlmApi vendorApi = llm.unwrap(SomeLlmApi.class);
return vendorApi.advancedAnalysis(data);
} catch (IllegalArgumentException e) {
return parseBasicResult(basicResult);
}
}
}
5.5. Provider Configuration
5.5.1. Initial Release
Implementations are free to support whichever LLM libraries and APIs they choose (e.g., Spring AI, LangChain4j, or vendor SDKs). Configuration of the LLM provider, if supported, is implementation-specific (properties, environment variables, or other mechanisms).
5.5.2. Future Releases
Future releases may introduce standardized configuration for provider selection and common parameters through Jakarta Config — analogous to how Jakarta Persistence handles multiple database providers — covering shared settings such as endpoints, authentication, model selection, temperature, and maximum output tokens.
For details on the current implementation, refer to your Jakarta Agentic AI implementation’s documentation.
6. Examples
This chapter provides informative end-to-end examples demonstrating common agent patterns and use cases. The examples focus on composition and integration style rather than production-ready completeness.
6.1. Simple Fraud Detection Agent
A basic agent that detects fraudulent transactions:
@Agent(description="Detects bank fraud transactions")
public class FraudDetectionAgent {
@Inject
private LargeLanguageModel model;
@Inject
private EntityManager entityManager;
@Inject
private AlertService alertService;
@Trigger
public void processTransaction(@Valid BankTransaction transaction) {
logger.info("Processing transaction: " + transaction.getId());
}
@Decision
public Result checkFraud(BankTransaction transaction) {
FraudAssessment assessment = model.query(
"""
Analyze this transaction for fraud:
Amount: {}
Merchant: {}
Location: {}
Respond with JSON: {"isFraud": true/false, "confidence": 0.0-1.0, "reason": "..."}
""",
FraudAssessment.class,
transaction.getAmount(),
transaction.getMerchant(),
transaction.getLocation()
);
Fraud details = assessment.isFraud()
? new Fraud(assessment.confidence(), assessment.reason())
: null;
return new Result(assessment.isFraud(), details);
}
@Action
public void alertBankSecurity(Fraud fraud) {
if (fraud.isSerious()) {
alertService.alertBankSecurity(fraud);
}
}
@Action
public void alertCustomer(Fraud fraud, BankTransaction transaction) {
Customer customer = getCustomer(transaction);
alertService.alertCustomer(fraud, transaction, customer);
}
@Outcome
@Transactional
public void finalizeTransaction(BankTransaction transaction, Fraud fraud) {
transaction.setStatus(TransactionStatus.SUSPECT);
entityManager.merge(transaction);
logger.info("Transaction marked as suspect: " + transaction.getId());
}
@HandleException
@Transactional
public void handleModelUnavailable(LLMException e, BankTransaction transaction) {
logger.warn("Fraud model unavailable for transaction: " + transaction.getId(), e);
// Compensate safely: route to manual review when model calls fail
transaction.setStatus(TransactionStatus.NEEDS_REVIEW);
entityManager.merge(transaction);
}
}
6.2. Documentation Generation Agent
An agent that monitors pull requests and generates documentation:
@Agent
@ApplicationScoped
public class DocumentationAgent {
@Inject
private LargeLanguageModel llm;
@Inject
private GitService gitService;
@Trigger
public void onPullRequest(PullRequestEvent prEvent) {
logger.info("Processing PR #" + prEvent.getNumber());
}
@Decision
public DocumentationPlan shouldGenerateDocs(PullRequestEvent prEvent) {
PullRequest pr = prEvent.getPullRequest();
DocumentationPlan plan = llm.query(
"""
Analyze this pull request and determine if documentation is needed.
Title: {}
Description: {}
Changed Files: {}
Respond with JSON:
{"needsDocumentation": true/false, "suggestedFiles": ["file1.md"]}
""",
DocumentationPlan.class,
pr.getTitle(),
pr.getDescription(),
String.join(", ", pr.getChangedFiles())
);
if (!plan.needsDocumentation()) {
logger.info("No documentation needed for PR #" + pr.getNumber());
return null; // Stop workflow
}
return plan;
}
@Action
public DocumentationFiles generateDocumentation(
PullRequestEvent prEvent,
DocumentationPlan plan
) {
PullRequest pr = prEvent.getPullRequest();
DocumentationFiles files = new DocumentationFiles();
for (String filename : plan.getSuggestedFiles()) {
String content = llm.query(
"Generate documentation for: {}. Diff: {}",
filename,
pr.getDiff()
);
files.addFile(filename, content);
}
return files;
}
@Outcome
public void createDocumentationPR(
PullRequestEvent originalPr,
DocumentationFiles files
) {
String branchName = "docs/pr-" + originalPr.getNumber();
gitService.createBranch(branchName);
gitService.commitFiles(branchName, files);
PullRequest docPr = gitService.createPullRequest(
branchName,
"main",
"Documentation for PR #" + originalPr.getNumber()
);
logger.info("Created documentation PR #" + docPr.getNumber());
}
@HandleException
public void handleGenerationFailure(LLMException e, PullRequestEvent prEvent) {
logger.warn("Documentation generation unavailable for PR #" +
prEvent.getNumber(), e);
gitService.addComment(
prEvent.getNumber(),
"Automatic documentation is temporarily unavailable. " +
"Please update docs manually for this PR."
);
}
}
6.3. Customer Support Agent
A support agent that handles customer inquiries:
@Agent
public class CustomerSupportAgent {
@Inject
private LargeLanguageModel llm;
@Inject
private KnowledgeBaseService knowledgeBase;
@Inject
private TicketService ticketService;
@Inject
private CustomerService customerService;
@Inject
private MessagingService messagingService;
private Customer customer;
@Trigger
public void onCustomerMessage(CustomerMessage message) {
customer = customerService.getCustomer(message.getCustomerId());
}
@Action
public SupportResponse generateResponse(CustomerMessage message) {
List<KnowledgeArticle> articles = knowledgeBase.search(message.getText(), 5);
String llmResponse = llm.query(
"""
You are a helpful customer support agent.
Customer: {}
Relevant Knowledge Base Articles:
{}
Customer's Latest Message: {}
Provide a helpful response. Respond with JSON:
{"response": "...", "canResolve": true/false, "needsEscalation": true/false}
""",
customer.getName(),
articles.stream().map(a -> a.getSummary()).collect(Collectors.joining("\n")),
message.getText()
);
return parseResponse(llmResponse);
}
@Action
public void sendResponse(SupportResponse response) {
messagingService.sendToCustomer(customer.getId(), response.getResponse());
}
@Outcome
public void finalizeInteraction(SupportResponse response) {
if (response.needsEscalation()) {
ticketService.escalate(customer.getId(), "Agent unable to resolve");
messagingService.sendToCustomer(
customer.getId(),
"I've escalated your issue to a specialist."
);
}
}
@HandleException
public void handleModelFailure(LLMException e, CustomerMessage message) {
logger.warn("Support response generation unavailable", e);
ticketService.escalate(customer.getId(), "Automated response unavailable");
}
}
6.4. Parameter Validation and Error Recovery Agent
An agent that validates input and recovers gracefully from validation and delivery failures:
@Agent
public class OnboardingAgent {
@Inject
private EmailService emailService;
// Constraint violations short-circuit the workflow and are recovered by
// @HandleException below.
@Trigger
public void onInvitation(@Valid @NotNull UserInvitation invitation) {
logger.info("Processing invitation for: " + invitation.getEmail());
}
@Decision
public boolean isEmailActive(UserInvitation invitation) {
// Format was validated by @Email on UserInvitation; here we confirm
// the address is actually reachable.
return emailService.isActiveEmail(invitation.getEmail());
}
@Action
public void sendWelcomeEmail(UserInvitation invitation) {
User invitee = invitation.getInvitee();
emailService.send(
invitation.getEmail(),
"Welcome " + invitee.getFirstName() + "!"
);
}
@Outcome
public void markAccepted(UserInvitation invitation) {
invitation.setStatus(InvitationStatus.ACCEPTED);
}
@HandleException
public void onInvalidInvitation(ConstraintViolationException e,
UserInvitation invitation) {
for (var v : e.getConstraintViolations()) {
logger.warn("Invalid: " + v.getPropertyPath() + " - " + v.getMessage());
}
invitation.setStatus(InvitationStatus.INVALID);
}
// Compensating recovery when the mail service cannot deliver.
@HandleException
public void onDeliveryFailure(EmailDeliveryException e,
UserInvitation invitation) {
logger.error("Unable to deliver welcome email", e);
invitation.setStatus(InvitationStatus.DELIVERY_FAILED);
}
}
6.5. Key Patterns Demonstrated
These examples demonstrate:
-
Different Scopes: default
@WorkflowScoped, explicit@ApplicationScoped -
LLM Integration: Using
LargeLanguageModelfor intelligent decisions -
Conditional Workflows: Decisions that stop or continue workflows
-
Multiple Actions: Sequential action execution
-
Error Handling: Graceful degradation and fallback strategies
-
CDI Integration: Dependency injection, transactions
-
Context Management: Maintaining state across workflow phases
-
Parameter Validation: Jakarta Validation constraints with failure recovery
-
Real-world Use Cases: Fraud detection, documentation generation, customer support
Bibliography
-
Java SE 17, https://docs.oracle.com/en/java/javase/17/
-
Jakarta EE Platform 10, https://jakarta.ee/specifications/platform/10/
-
Jakarta Contexts and Dependency Injection 4.0, https://jakarta.ee/specifications/cdi/4.0/
-
Jakarta Validation 3.0, https://jakarta.ee/specifications/bean-validation/3.0/
-
Jakarta JSON Binding 3.0, https://jakarta.ee/specifications/jsonb/3.0/