diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4465fbc..710659e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,21 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [2.1.2] - 2026-01-07
+
+### Fixed
+
+- **Gateway Mode clientId not sent in request body**: Fixed `getPolicyApprovedContext()` to auto-populate `client_id` in request body from config when not explicitly provided
+ - Server requires `client_id` in JSON body for `/api/policy/pre-check` endpoint
+ - Previously only sent as header (X-Client-ID), causing "client_id field is required" errors
+ - Now matches Go SDK behavior which auto-populates from `config.ClientID`
+ - Affects all Gateway Mode pre-check calls
+
+- **executePlan() using non-existent endpoint**: Fixed `executePlan()` to use correct Agent API endpoint
+ - Changed from `/api/v1/orchestrator/plan/{planId}/execute` (404) to `/api/request` with `request_type: "execute-plan"`
+ - Now matches Go SDK pattern for plan execution
+ - Fixes MAP (Multi-Agent Planning) two-step execution flow
+
## [2.1.1] - 2026-01-06
### Fixed
diff --git a/pom.xml b/pom.xml
index 3bd3976..2cb1ecc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.getaxonflow
axonflow-sdk
- 2.1.1
+ 2.1.2
jar
AxonFlow Java SDK
diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java
index f1f6f03..4817bc4 100644
--- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java
+++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java
@@ -261,8 +261,23 @@ public PolicyApprovalResult getPolicyApprovedContext(PolicyApprovalRequest reque
// Gateway Mode: Let server decide if credentials are required based on DEPLOYMENT_MODE
// Community/self-hosted deployments work without credentials
+ // Auto-populate clientId from config if not set in request (matches Go SDK behavior)
+ PolicyApprovalRequest effectiveRequest = request;
+ if ((request.getClientId() == null || request.getClientId().isEmpty())
+ && config.getClientId() != null && !config.getClientId().isEmpty()) {
+ Map ctx = request.getContext();
+ effectiveRequest = PolicyApprovalRequest.builder()
+ .userToken(request.getUserToken())
+ .query(request.getQuery())
+ .dataSources(request.getDataSources())
+ .context(ctx == null || ctx.isEmpty() ? null : ctx)
+ .clientId(config.getClientId())
+ .build();
+ }
+
+ final PolicyApprovalRequest finalRequest = effectiveRequest;
return retryExecutor.execute(() -> {
- Request httpRequest = buildRequest("POST", "/api/policy/pre-check", request);
+ Request httpRequest = buildRequest("POST", "/api/policy/pre-check", finalRequest);
try (Response response = httpClient.newCall(httpRequest).execute()) {
PolicyApprovalResult result = parseResponse(response, PolicyApprovalResult.class);
@@ -648,14 +663,55 @@ public PlanResponse executePlan(String planId) {
Objects.requireNonNull(planId, "planId cannot be null");
return retryExecutor.execute(() -> {
- Request httpRequest = buildRequest("POST",
- "/api/v1/orchestrator/plan/" + planId + "/execute", null);
+ // Build agent request format - like generatePlan but with request_type "execute-plan"
+ String userToken = config.getClientId() != null ? config.getClientId() : "default";
+ String clientId = config.getClientId() != null ? config.getClientId() : "default";
+
+ Map agentRequest = new java.util.HashMap<>();
+ agentRequest.put("query", "");
+ agentRequest.put("user_token", userToken);
+ agentRequest.put("client_id", clientId);
+ agentRequest.put("request_type", "execute-plan");
+ agentRequest.put("context", Map.of("plan_id", planId));
+
+ Request httpRequest = buildRequest("POST", "/api/request", agentRequest);
try (Response response = httpClient.newCall(httpRequest).execute()) {
- return parseResponse(response, PlanResponse.class);
+ return parseExecutePlanResponse(response, planId);
}
}, "executePlan");
}
+ /**
+ * Parses the execute plan response.
+ */
+ @SuppressWarnings("unchecked")
+ private PlanResponse parseExecutePlanResponse(Response response, String planId) throws IOException {
+ handleErrorResponse(response);
+
+ ResponseBody body = response.body();
+ if (body == null) {
+ throw new AxonFlowException("Empty response body", response.code(), null);
+ }
+
+ String json = body.string();
+ Map agentResponse = objectMapper.readValue(json,
+ new TypeReference