Cancel NICE CXone Flow Runs Programmatically with Java
What You Will Build
- A Java client that terminates active CXone Flow runs by sending atomic cancel requests with structured payloads, rollback directives, and validation constraints.
- This implementation uses the NICE CXone Flow REST API (
/api/v2/flows/runs/{runId}/cancel) and standard HTTP clients without relying on third-party SDK wrappers. - The tutorial covers Java 17+,
java.net.http.HttpClient, Jackson JSON binding, OAuth 2.0 token management, constraint validation, latency tracking, and audit logging.
Prerequisites
- OAuth client type: Confidential client registered in CXone OAuth Applications
- Required scopes:
flows:write(for cancel operations),flows:read(for run status validation) - API version: CXone Flow API v2
- Runtime: Java 17 or higher
- Dependencies:
com.fasterxml.jackson.core:jackson-databind:2.15.2(for JSON serialization/deserialization) - Environment variables:
CXONE_DOMAIN,CXONE_CLIENT_ID,CXONE_CLIENT_SECRET
Authentication Setup
CXone uses standard OAuth 2.0 Client Credentials flow. The access token expires after 3600 seconds and must be cached or refreshed before expiration. The following code demonstrates token acquisition with expiration tracking and automatic refresh logic.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class CxoneAuthManager {
private final String domain;
private final String clientId;
private final String clientSecret;
private final HttpClient httpClient;
private final ObjectMapper mapper;
private final Map<String, Object> tokenCache = new ConcurrentHashMap<>();
public CxoneAuthManager(String domain, String clientId, String clientSecret) {
this.domain = domain;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.httpClient = HttpClient.newBuilder().build();
this.mapper = new ObjectMapper();
}
public String getAccessToken() throws Exception {
Instant now = Instant.now();
Instant expiresAt = Instant.ofEpochSecond((Long) tokenCache.get("expires_at"));
if (tokenCache.containsKey("access_token") && expiresAt.isAfter(now)) {
return (String) tokenCache.get("access_token");
}
String requestBody = Map.of(
"grant_type", "client_credentials",
"client_id", clientId,
"client_secret", clientSecret,
"scope", "flows:write flows:read"
).entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.reduce((a, b) -> a + "&" + b)
.orElse("");
HttpRequest request = HttpRequest.newBuilder()
.uri(java.net.URI.create("https://" + domain + ".api.cxone.com/oauth/token"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("OAuth token request failed with status " + response.statusCode() + ": " + response.body());
}
JsonNode json = mapper.readTree(response.body());
String token = json.get("access_token").asText();
long expiresIn = json.get("expires_in").asLong();
tokenCache.put("access_token", token);
tokenCache.put("expires_at", now.getEpochSecond() + expiresIn - 30); // 30s buffer
return token;
}
}
Implementation
Step 1: Define Cancel Payload Schema and Reason Matrix
CXone Flow cancel operations accept a JSON body. The orchestration engine validates the payload against internal constraints before terminating the run. You must structure the request with a cancellation reason, rollback directives, and cleanup flags. The following records enforce schema compliance and map to the API contract.
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public record CancelPayload(
@JsonProperty("cancelReason") String cancelReason,
@JsonProperty("rollbackState") boolean rollbackState,
@JsonProperty("forceCleanup") boolean forceCleanup,
@JsonProperty("maxCancellationWindowSeconds") Integer maxCancellationWindowSeconds
) {}
public enum CancellationReason {
OPERATOR_INITIATED("operator_initiated"),
SYSTEM_TIMEOUT("system_timeout"),
DEPENDENT_FAILURE("dependent_failure"),
SCALING_ROLLBACK("scaling_rollback");
private final String apiValue;
CancellationReason(String apiValue) { this.apiValue = apiValue; }
public String getApiValue() { return apiValue; }
}
Step 2: Validate Run Status and Orchestration Constraints
Before sending the cancel request, you must verify that the run is still active and falls within the maximum cancellation window. CXone returns 409 Conflict if the run has already terminated. The validation pipeline checks run status, calculates elapsed time, and verifies dependent process states to prevent orphaned transactions.
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
public class RunValidator {
private final String domain;
private final HttpClient httpClient;
private final ObjectMapper mapper;
public RunValidator(String domain, HttpClient httpClient, ObjectMapper mapper) {
this.domain = domain;
this.httpClient = httpClient;
this.mapper = mapper;
}
public Map<String, Object> validateRunForCancellation(String runId, String token, int maxWindowSeconds) throws Exception {
HttpRequest statusRequest = HttpRequest.newBuilder()
.uri(java.net.URI.create("https://" + domain + ".api.cxone.com/api/v2/flows/runs/" + runId))
.header("Authorization", "Bearer " + token)
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> statusResponse = httpClient.send(statusRequest, HttpResponse.BodyHandlers.ofString());
if (statusResponse.statusCode() == 404) {
throw new IllegalArgumentException("Run ID " + runId + " does not exist");
}
if (statusResponse.statusCode() != 200) {
throw new RuntimeException("Failed to fetch run status: " + statusResponse.body());
}
JsonNode runData = mapper.readTree(statusResponse.body());
String status = runData.path("status").asText();
Instant startTime = Instant.ofEpochMilli(runData.path("start_time").asLong());
long elapsedSeconds = Instant.now().getEpochSecond() - startTime.getEpochSecond();
if (!"running".equals(status) && !"queued".equals(status)) {
throw new IllegalStateException("Run is already in terminal state: " + status);
}
if (elapsedSeconds > maxWindowSeconds) {
throw new IllegalStateException("Run exceeds maximum cancellation window of " + maxWindowSeconds + " seconds. Elapsed: " + elapsedSeconds);
}
return Map.of(
"status", status,
"elapsedSeconds", elapsedSeconds,
"runData", runData
);
}
}
Step 3: Execute Atomic Cancel POST with Retry and Format Verification
The cancel operation uses an atomic POST request. CXone processes the request synchronously and returns 200 OK upon successful termination initiation. You must implement retry logic for 429 Too Many Requests responses and verify the response payload matches the expected schema.
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class CxoneFlowCanceler {
private final String domain;
private final HttpClient httpClient;
private final ObjectMapper mapper;
private final CxoneAuthManager authManager;
private final RunValidator validator;
public CxoneFlowCanceler(String domain, CxoneAuthManager authManager, RunValidator validator) {
this.domain = domain;
this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
this.mapper = new ObjectMapper();
this.authManager = authManager;
this.validator = validator;
}
public CancelResult cancelRun(String runId, CancellationReason reason, boolean rollbackState, boolean forceCleanup, int maxWindowSeconds) throws Exception {
String token = authManager.getAccessToken();
Map<String, Object> validation = validator.validateRunForCancellation(runId, token, maxWindowSeconds);
CancelPayload payload = new CancelPayload(
reason.getApiValue(),
rollbackState,
forceCleanup,
maxWindowSeconds
);
String jsonBody = mapper.writeValueAsString(payload);
long startNanos = System.nanoTime();
int maxRetries = 3;
Exception lastException = null;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
HttpRequest request = HttpRequest.newBuilder()
.uri(java.net.URI.create("https://" + domain + ".api.cxone.com/api/v2/flows/runs/" + runId + "/cancel"))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 429 && attempt < maxRetries) {
long retryAfter = parseRetryAfter(response);
Thread.sleep(retryAfter);
continue;
}
if (response.statusCode() >= 400) {
throw new RuntimeException("Cancel request failed with " + response.statusCode() + ": " + response.body());
}
long latencyNanos = System.nanoTime() - startNanos;
JsonNode responseBody = mapper.readTree(response.body());
return new CancelResult(
runId,
response.statusCode(),
responseBody.has("rollback_success") ? responseBody.get("rollback_success").asBoolean() : false,
Duration.ofNanos(latencyNanos),
responseBody
);
}
throw lastException != null ? lastException : new RuntimeException("Max retries exceeded for cancel operation");
}
private long parseRetryAfter(HttpResponse<String> response) {
String header = response.headers().firstValue("Retry-After").orElse("2");
try {
return Long.parseLong(header) * 1000;
} catch (NumberFormatException e) {
return ThreadLocalRandom.current().nextLong(1000, 3000);
}
}
public record CancelResult(String runId, int statusCode, boolean rollbackSuccess, Duration latency, JsonNode responsePayload) {}
}
Step 4: Audit Logging, Latency Tracking, and Callback Synchronization
Production systems require governance trails and external monitoring alignment. The following handler demonstrates how to capture cancel events, compute rollback success rates, and dispatch synchronization callbacks to external observability platforms.
import java.time.Instant;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
public class CancelAuditTracker {
private final AtomicInteger totalCancels = new AtomicInteger(0);
private final AtomicInteger successfulRollbacks = new AtomicInteger(0);
private final Consumer<CancelEvent> callbackHandler;
public CancelAuditTracker(Consumer<CancelEvent> callbackHandler) {
this.callbackHandler = callbackHandler;
}
public void recordCancel(CxoneFlowCanceler.CancelResult result, String operatorId) {
totalCancels.incrementAndGet();
if (result.rollbackSuccess()) {
successfulRollbacks.incrementAndGet();
}
CancelEvent event = new CancelEvent(
Instant.now(),
result.runId(),
result.statusCode(),
result.rollbackSuccess(),
result.latency().toMillis(),
operatorId,
totalCancels.get(),
successfulRollbacks.get()
);
callbackHandler.accept(event);
System.out.println("AUDIT: " + mapper.writeValueAsString(event));
}
private final ObjectMapper mapper = new ObjectMapper();
public record CancelEvent(
Instant timestamp,
String runId,
int statusCode,
boolean rollbackSuccess,
long latencyMs,
String operatorId,
int totalAttempts,
int successfulRollbacks
) {}
}
Complete Working Example
The following module integrates authentication, validation, cancellation execution, and audit tracking into a single runnable class. Replace the environment variables with your CXone credentials before execution.
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.function.Consumer;
public class FlowCancelOrchestrator {
public static void main(String[] args) {
String domain = System.getenv("CXONE_DOMAIN");
String clientId = System.getenv("CXONE_CLIENT_ID");
String clientSecret = System.getenv("CXONE_CLIENT_SECRET");
String targetRunId = System.getenv("TARGET_RUN_ID");
if (domain == null || clientId == null || clientSecret == null || targetRunId == null) {
System.err.println("Missing required environment variables: CXONE_DOMAIN, CXONE_CLIENT_ID, CXONE_CLIENT_SECRET, TARGET_RUN_ID");
System.exit(1);
}
CxoneAuthManager auth = new CxoneAuthManager(domain, clientId, clientSecret);
HttpClient httpClient = java.net.http.HttpClient.newBuilder().build();
ObjectMapper mapper = new ObjectMapper();
RunValidator validator = new RunValidator(domain, httpClient, mapper);
CxoneFlowCanceler canceler = new CxoneFlowCanceler(domain, auth, validator);
Consumer<CancelAuditTracker.CancelEvent> monitoringCallback = event -> {
System.out.println("MONITORING_SYNC: Run " + event.runId() + " cancelled in " + event.latencyMs() + "ms. Rollback: " + event.rollbackSuccess());
};
CancelAuditTracker auditTracker = new CancelAuditTracker(monitoringCallback);
try {
CxoneFlowCanceler.CancelResult result = canceler.cancelRun(
targetRunId,
CancellationReason.OPERATOR_INITIATED,
true,
true,
30
);
auditTracker.recordCancel(result, "system_automator");
System.out.println("Cancellation completed successfully. Rollback status: " + result.rollbackSuccess());
} catch (Exception e) {
System.err.println("Cancellation failed: " + e.getMessage());
e.printStackTrace();
}
}
}
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired, malformed, or missing the
flows:writescope. - Fix: Verify the token cache expiration logic. Ensure the
scopeparameter in the/oauth/tokenrequest includesflows:write. Revoke and regenerate the client secret if rotation occurred. - Code verification: The
CxoneAuthManagerincludes a 30-second expiration buffer. If failures persist, force a cache clear by removingtokenCache.clear()before token acquisition.
Error: 403 Forbidden
- Cause: The OAuth application lacks Flow API permissions, or the tenant administrator disabled programmatic run cancellation.
- Fix: Navigate to CXone OAuth Applications and confirm the client has
flows:writeandflows:readscopes granted. Verify the application is not restricted to specific user roles that lack Flow management privileges.
Error: 409 Conflict
- Cause: The target run has already transitioned to a terminal state (
completed,failed,cancelled) before the cancel request reached the orchestration engine. - Fix: Implement the
RunValidatorstatus check prior to execution. The validation pipeline explicitly rejects terminal states. If races occur during high concurrency, wrap the cancel call in a retry block that treats409as a successful idempotent outcome.
Error: 422 Unprocessable Entity
- Cause: The JSON payload violates CXone schema constraints. Missing
cancelReason, invalid enum value, orrollbackStateset totruewhen the run contains non-rollbackable steps. - Fix: Validate the
CancelPayloadstructure against the CXone Flow API schema. EnsurerollbackStatealigns with the flow design. CXone rejects rollback directives for flows containing external API calls or database mutations that lack compensating transactions.
Error: 500 Internal Server Error
- Cause: Orchestration engine timeout during state rollback or dependent process verification.
- Fix: Implement exponential backoff retry logic. The provided
cancelRunmethod includes a 3-attempt retry loop. If the engine consistently returns500, reducemaxCancellationWindowSecondsand verify that dependent flows are not holding distributed locks.