Migrating Dialogflow Intents to NICE Cognigy.AI Using Java
What You Will Build
A Java-based converter that exports Dialogflow agent data via the REST API, parses JSON structures to extract intents, entities, and training phrases, maps Dialogflow elements to Cognigy dialog nodes and slot types using a transformation engine, generates Cognigy project files, validates the generated project against schema rules, and implements a dry-run mode to preview changes before deployment. This tutorial uses the Dialogflow v2 REST API and the NICE Cognigy.AI v1 API. The implementation is written in Java 17.
Prerequisites
- Dialogflow service account or OAuth client with scopes
https://www.googleapis.com/auth/dialogflowandhttps://www.googleapis.com/auth/cloud-platform - Cognigy.AI API key with
adminordeveloperrole - Java 17 or later
- Maven dependencies:
com.fasterxml.jackson.core:jackson-databind:2.15.2,com.networknt:json-schema-validator:1.0.97,org.slf4j:slf4j-simple:2.0.7 - Access to the Dialogflow REST API base
https://dialogflow.googleapis.com/v2 - Access to the Cognigy.AI REST API base
https://api.cognigy.ai/v1
Authentication Setup
Dialogflow requires OAuth 2.0 access tokens for API calls. The converter fetches a token using the client credentials flow and caches it until expiration. Cognigy.AI accepts API keys passed as bearer tokens in the Authorization header.
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class AuthManager {
private static final String DIALOGFLOW_TOKEN_URL = "https://oauth2.googleapis.com/token";
private static final String COGNIGY_BASE_URL = "https://api.cognigy.ai/v1";
private final HttpClient httpClient;
private final ObjectMapper mapper;
private String cachedToken;
private Instant tokenExpiry;
private final String clientId;
private final String clientSecret;
private final String cognigyApiKey;
public AuthManager(String clientId, String clientSecret, String cognigyApiKey) {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(10))
.build();
this.mapper = new ObjectMapper();
this.clientId = clientId;
this.clientSecret = clientSecret;
this.cognigyApiKey = cognigyApiKey;
this.tokenExpiry = Instant.now();
}
public String getDialogflowToken() throws IOException, InterruptedException {
if (cachedToken != null && Instant.now().isBefore(tokenExpiry)) {
return cachedToken;
}
String requestBody = String.format(
"grant_type=client_credentials&client_id=%s&client_secret=%s",
clientId, clientSecret
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(DIALOGFLOW_TOKEN_URL))
.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 IOException("Dialogflow token fetch failed with status " + response.statusCode());
}
JsonNode json = mapper.readTree(response.body());
this.cachedToken = json.get("access_token").asText();
this.tokenExpiry = Instant.now().plusSeconds(json.get("expires_in").asLong());
return cachedToken;
}
public String getCognigyToken() {
return cognigyApiKey;
}
}
The AuthManager caches the Dialogflow token and refreshes it only when expired. The Cognigy API key is returned directly. Both tokens are used in subsequent API calls.
Implementation
Step 1: Export Dialogflow Agent Data
The converter retrieves intents from Dialogflow using the v2 REST API. The endpoint supports pagination via pageToken. The request includes exponential backoff for 429 rate limit responses.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class DialogflowExporter {
private final HttpClient httpClient;
private final ObjectMapper mapper;
private final AuthManager authManager;
private final String projectId;
public DialogflowExporter(HttpClient httpClient, ObjectMapper mapper, AuthManager authManager, String projectId) {
this.httpClient = httpClient;
this.mapper = mapper;
this.authManager = authManager;
this.projectId = projectId;
}
public List<JsonNode> exportIntents() throws Exception {
List<JsonNode> allIntents = new ArrayList<>();
String pageToken = null;
int maxRetries = 3;
do {
String url = String.format(
"https://dialogflow.googleapis.com/v2/projects/%s/locations/global/intents",
projectId
);
if (pageToken != null) {
url += "?pageToken=" + pageToken;
}
String token = authManager.getDialogflowToken();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.GET()
.build();
int retryCount = 0;
HttpResponse<String> response = null;
while (retryCount < maxRetries) {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 429) {
long delay = 1000L * (long) Math.pow(2, retryCount);
Thread.sleep(delay);
retryCount++;
} else {
break;
}
}
if (response.statusCode() != 200) {
throw new RuntimeException("Dialogflow API failed with status " + response.statusCode() + ": " + response.body());
}
JsonNode root = mapper.readTree(response.body());
JsonNode intentsNode = root.get("intents");
if (intentsNode != null && intentsNode.isArray()) {
for (JsonNode intent : intentsNode) {
allIntents.add(intent);
}
}
pageToken = root.has("nextPageToken") ? root.get("nextPageToken").asText() : null;
} while (pageToken != null);
return allIntents;
}
}
The exporter handles pagination automatically. The pageToken loop continues until Dialogflow returns an empty token. The retry logic sleeps exponentially on 429 responses. The required OAuth scope is https://www.googleapis.com/auth/dialogflow.
Step 2: Parse and Extract Intents, Entities, and Training Phrases
Dialogflow returns intents with nested training phrases and parameters. The parser extracts these fields into flat Java records for transformation.
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
public record ParsedIntent(String displayName, List<String> trainingPhrases, List<ParsedParameter> parameters) {}
public record ParsedParameter(String name, String type, List<String> values) {}
public class IntentParser {
private final com.fasterxml.jackson.databind.ObjectMapper mapper;
public IntentParser(com.fasterxml.jackson.databind.ObjectMapper mapper) {
this.mapper = mapper;
}
public List<ParsedIntent> parse(List<JsonNode> dialogflowIntents) {
List<ParsedIntent> parsed = new ArrayList<>();
for (JsonNode node : dialogflowIntents) {
String name = node.has("displayName") ? node.get("displayName").asText() : "unnamed_intent";
List<String> phrases = extractTrainingPhrases(node);
List<ParsedParameter> params = extractParameters(node);
parsed.add(new ParsedIntent(name, phrases, params));
}
return parsed;
}
private List<String> extractTrainingPhrases(JsonNode intentNode) {
List<String> phrases = new ArrayList<>();
JsonNode trainingPhrases = intentNode.get("trainingPhrases");
if (trainingPhrases != null && trainingPhrases.isArray()) {
for (JsonNode phrase : trainingPhrases) {
JsonNode parts = phrase.get("parts");
if (parts != null && parts.isArray()) {
StringBuilder sb = new StringBuilder();
for (JsonNode part : parts) {
sb.append(part.get("text").asText());
}
phrases.add(sb.toString());
}
}
}
return phrases;
}
private List<ParsedParameter> extractParameters(JsonNode intentNode) {
List<ParsedParameter> params = new ArrayList<>();
JsonNode parameters = intentNode.get("parameters");
if (parameters != null && parameters.isObject()) {
parameters.fields().forEachRemaining(entry -> {
String paramName = entry.getKey();
JsonNode paramNode = entry.getValue();
String type = paramNode.has("type") ? paramNode.get("type").asText() : "@sys.any";
List<String> values = new ArrayList<>();
if (paramNode.has("value")) {
String val = paramNode.get("value").asText();
if (!val.isEmpty()) {
values.add(val);
}
}
params.add(new ParsedParameter(paramName, type, values));
});
}
return params;
}
}
The parser flattens Dialogflow’s nested trainingPhrases array into a single list of strings. It extracts parameters into ParsedParameter records. This structure prepares the data for Cognigy mapping.
Step 3: Transformation Engine
The transformation engine maps Dialogflow elements to Cognigy.AI structures. Dialogflow intents become Cognigy dialog nodes. Dialogflow parameters become Cognigy slot types. The engine generates JSON payloads compatible with Cognigy’s import schema.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
public class CognigyTransformer {
private final ObjectMapper mapper;
public CognigyTransformer(ObjectMapper mapper) {
this.mapper = mapper;
}
public Map<String, Object> transformToCognigy(List<ParsedIntent> intents) {
Map<String, Object> project = new HashMap<>();
List<Object> nodes = new ArrayList<>();
List<Object> slots = new ArrayList<>();
Map<String, Boolean> processedSlots = new HashMap<>();
for (ParsedIntent intent : intents) {
Map<String, Object> node = new HashMap<>();
node.put("name", intent.displayName());
node.put("type", "dialog");
node.put("phrases", intent.trainingPhrases());
List<Map<String, Object>> nodeSlots = new ArrayList<>();
for (ParsedParameter param : intent.parameters()) {
String slotKey = param.name();
if (!processedSlots.containsKey(slotKey)) {
processedSlots.put(slotKey, true);
Map<String, Object> slot = new HashMap<>();
slot.put("name", slotKey);
slot.put("type", mapDialogflowTypeToCognigy(param.type()));
slot.put("values", param.values());
slots.add(slot);
}
Map<String, Object> slotRef = new HashMap<>();
slotRef.put("name", slotKey);
slotRef.put("required", true);
nodeSlots.add(slotRef);
}
if (!nodeSlots.isEmpty()) {
node.put("slots", nodeSlots);
}
nodes.add(node);
}
project.put("nodes", nodes);
project.put("slots", slots);
return project;
}
private String mapDialogflowTypeToCognigy(String dfType) {
if (dfType.contains("number") || dfType.contains("integer")) return "number";
if (dfType.contains("date") || dfType.contains("time")) return "date";
if (dfType.contains("location") || dfType.contains("address")) return "location";
return "text";
}
}
The transformer creates Cognigy node objects with name, type, phrases, and slots. It deduplicates slots across intents to avoid redundant definitions. The mapDialogflowTypeToCognigy method translates Dialogflow system types to Cognigy slot types.
Step 4: Generate, Validate, and Deploy with Dry-Run Mode
The final step validates the transformed JSON against a Cognigy schema, supports a dry-run flag, and POSTs to the Cognigy API when enabled.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.input.JsonSchemaFactoryLoader;
public class CognigyDeployer {
private final HttpClient httpClient;
private final ObjectMapper mapper;
private final AuthManager authManager;
private final String cognigyProjectId;
private final boolean dryRun;
private final JsonSchema cognigySchema;
public CognigyDeployer(HttpClient httpClient, ObjectMapper mapper, AuthManager authManager,
String cognigyProjectId, boolean dryRun) {
this.httpClient = httpClient;
this.mapper = mapper;
this.authManager = authManager;
this.cognigyProjectId = cognigyProjectId;
this.dryRun = dryRun;
this.cognigySchema = buildSchema();
}
private JsonSchema buildSchema() {
String schemaJson = """
{
"type": "object",
"required": ["nodes", "slots"],
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "type", "phrases"],
"properties": {
"name": {"type": "string"},
"type": {"type": "string", "enum": ["dialog", "menu", "form"]},
"phrases": {"type": "array", "items": {"type": "string"}},
"slots": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": {"type": "string"},
"required": {"type": "boolean"}
}
}
}
}
}
},
"slots": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "type", "values"],
"properties": {
"name": {"type": "string"},
"type": {"type": "string"},
"values": {"type": "array", "items": {"type": "string"}}
}
}
}
}
}
""";
return JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)
.getSchema(schemaJson);
}
public void deploy(Map<String, Object> projectData) throws Exception {
String jsonPayload = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(projectData);
// Validate against schema
JsonNode node = mapper.readTree(jsonPayload);
var errors = cognigySchema.validate(node);
if (!errors.isEmpty()) {
throw new IllegalArgumentException("Cognigy schema validation failed: " + errors);
}
if (dryRun) {
System.out.println("[DRY-RUN] Generated Cognigy project payload:");
System.out.println(jsonPayload);
return;
}
// Deploy slots
if (projectData.containsKey("slots")) {
for (Object slot : (java.util.List<?>) projectData.get("slots")) {
postToCognigy("/slots", mapper.writeValueAsString(slot));
}
}
// Deploy nodes
if (projectData.containsKey("nodes")) {
for (Object nodeObj : (java.util.List<?>) projectData.get("nodes")) {
postToCognigy("/nodes", mapper.writeValueAsString(nodeObj));
}
}
}
private void postToCognigy(String path, String payload) throws Exception {
String url = String.format("https://api.cognigy.ai/v1%s?projectId=%s", path, cognigyProjectId);
String token = authManager.getCognigyToken();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new RuntimeException("Cognigy API call failed with status " + response.statusCode() + ": " + response.body());
}
}
}
The deployer validates the transformed payload using networknt-json-schema-validator. The dry-run flag prevents API calls and prints the JSON instead. When active, it POSTs slots and nodes to Cognigy using the Authorization: Bearer <api_key> header. The Cognigy API does not require OAuth scopes; it uses API key authentication.
Complete Working Example
The following class orchestrates the full migration pipeline. Replace placeholder credentials before execution.
import java.net.http.HttpClient;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
public class DialogflowToCognigyConverter {
public static void main(String[] args) {
try {
// Configuration
String dialogflowClientId = "YOUR_DIALOGFLOW_CLIENT_ID";
String dialogflowClientSecret = "YOUR_DIALOGFLOW_CLIENT_SECRET";
String cognigyApiKey = "YOUR_COGNIGY_API_KEY";
String dialogflowProjectId = "your-gcp-project-id";
String cognigyProjectId = "your-cognigy-project-id";
boolean dryRun = true; // Set to false to deploy
// Initialize dependencies
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(15))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
ObjectMapper mapper = new ObjectMapper();
AuthManager authManager = new AuthManager(dialogflowClientId, dialogflowClientSecret, cognigyApiKey);
// Step 1: Export Dialogflow intents
DialogflowExporter exporter = new DialogflowExporter(httpClient, mapper, authManager, dialogflowProjectId);
List<com.fasterxml.jackson.databind.JsonNode> rawIntents = exporter.exportIntents();
System.out.println("Exported " + rawIntents.size() + " Dialogflow intents.");
// Step 2: Parse structures
IntentParser parser = new IntentParser(mapper);
List<ParsedIntent> parsedIntents = parser.parse(rawIntents);
// Step 3: Transform to Cognigy format
CognigyTransformer transformer = new CognigyTransformer(mapper);
java.util.Map<String, Object> cognigyProject = transformer.transformToCognigy(parsedIntents);
// Step 4: Validate and deploy
CognigyDeployer deployer = new CognigyDeployer(httpClient, mapper, authManager, cognigyProjectId, dryRun);
deployer.deploy(cognigyProject);
System.out.println("Migration pipeline completed successfully.");
} catch (Exception e) {
System.err.println("Pipeline failed: " + e.getMessage());
e.printStackTrace();
}
}
}
This script runs the full pipeline from extraction to deployment. Set dryRun to false only after verifying the output matches your Cognigy project requirements.
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Expired Dialogflow token or invalid Cognigy API key.
- Fix: Ensure the
AuthManagerrefreshes tokens before expiration. Verify the Cognigy key hasadminordeveloperpermissions in the Cognigy portal. - Code: The
AuthManagercheckstokenExpirybefore reuse. If the key is invalid, Cognigy returns 401. Regenerate the key under Settings > API Keys.
Error: 429 Too Many Requests
- Cause: Dialogflow enforces per-minute rate limits on list endpoints.
- Fix: The
DialogflowExporterimplements exponential backoff. If failures persist, reduce batch size or add a fixed delay between pages. - Code: The retry loop sleeps
1000 * 2^retryCountmilliseconds. IncreasemaxRetriesif your workload requires it.
Error: Schema Validation Failed
- Cause: Missing required fields in the transformed JSON, such as
name,type, orphrases. - Fix: Ensure Dialogflow intents contain valid training phrases. Empty intents cause validation failures. Filter out intents with zero phrases before transformation.
- Code: Add a check in
CognigyTransformer.transformToCognigy:if (intent.trainingPhrases().isEmpty()) continue;
Error: 400 Bad Request on Cognigy API
- Cause: Slot names conflict with existing Cognigy slots, or node names exceed character limits.
- Fix: Prefix migrated slots with a namespace like
df_to avoid collisions. Trim node names to 64 characters. - Code: Modify the transformer to sanitize names:
slot.put("name", "df_" + slotKey.toLowerCase().replace(" ", "_"));