Skip to content

Add a new typed action

You want to insert a new step into the pipeline and have GOAP route through it automatically — no hand-wired call order. The planner derives the sequence from what each action consumes and produces, so you add a typed producer and re-point whatever should consume it.

This is the Lab 2 shape: inserting a research step between shortlist and assemble.

Lab 2 vs the final structure

The snippets below show the end-state structure (main), where each agent method is a thin wrapper delegating to a shared ConfPlanningCapabilities @Service (steps.research(...)). That service is introduced in Lab 5. In Lab 2 there is no steps field — the prompt body and id resolution live inline in ConfPlannerAgent.researchSessions. The GOAP lesson is identical either way; only where the body lives differs.

1. Add the domain records the action moves

Domain types are immutable Java records under domain/. For a research step:

public record SessionInsight(Session session, String whyRelevant, double matchScore) {}
public record ResearchedSessions(List<SessionInsight> insights) {}

Keep the id-only idiom: the model emits ids, plain code resolves ids back to Session from the catalog.

2. Declare the @Action on the agent

Add the method to the agent class (e.g. ConfPlannerAgent). Its parameter types are what it consumes; its return type is what it produces:

@Action
ResearchedSessions researchSessions(CandidateSessions candidates, Ai ai) {
    return steps.research(candidates, ai);
}

Put the prompt body and id resolution in the shared ConfPlanningCapabilities @Service if more than one agent will reuse it; keep the agent method a thin wrapper.

3. Re-point the consumer to the new type

Change the downstream action to consume the new type, so the planner must run the new step first:

// was: DraftSchedule assembleSchedule(AttendeeProfile profile, CandidateSessions candidates, Ai ai)
DraftSchedule assembleSchedule(AttendeeProfile profile, ResearchedSessions researched, Ai ai) { ... }

Do not call researchSessions directly from assembleSchedule; let the types drive the order. (See goal-oriented planning for why.)

4. Build and read the plan

./mvnw -q verify
./mvnw spring-boot:run
x "I'm a senior platform engineer into Kubernetes, resilience and DevEx" -p -r

The planning log should now read … → shortlist → research → assemble, with the new action between the others purely because research produces what assemble now consumes.

If you want the action to use a web tool

@Action has no toolGroups member. Add the tool group on the prompt runner:

ai.withDefaultLlm().withToolGroup(CoreToolGroups.WEB).creating(T.class).fromPrompt(prompt);

CoreToolGroups.WEB is the string "web". The tool group must be provided by an MCP tool server at runtime; the default no-Docker lab path does not wire one.

If the producer can fail and you want a clean replan

If the action may return null for a missing dependency, the planner marks the produces/consumes link unsatisfied and replans rather than crashing. To make a step re-runnable as part of an invariant loop, set canRerun = true — see Add a guardrail.


For the @Action / @AchievesGoal / @Condition parameters, see the annotations reference. For why goal-oriented planning beats a hand-coded sequence, see About GOAP planning.