Create Journey
createJourneyCreate or replace an authentication journey in PingOne. Define nodes with human-readable IDs that are automatically converted to UUIDs and map terminal outcomes.
Instructions
Create or replace an authentication journey (upsert operation — if a journey with the same name already exists, it is overwritten). Node IDs can be human-readable (e.g., "login-page") and will be automatically transformed to UUIDs. Use "success" or "failure" as connection targets for terminal nodes. Returns the mapping of original IDs to generated UUIDs.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| realm | Yes | The realm to create the journey in | |
| journeyName | Yes | The name of the journey | |
| description | No | Admin-facing description of the journey | |
| identityResource | No | The identity resource that the journey authenticates against. Expected format: "managed/<realm>_<objectType>" (e.g., "managed/alpha_user", "managed/bravo_role"). | |
| journeyData | Yes | The journey structure |
Implementation Reference
- src/tools/am/createJourney.ts:64-119 (handler)The toolFunction that executes the createJourney tool logic: validates connection targets, generates node ID mapping, transforms IDs, builds API payload, makes PUT request to AM API, and returns the result with node ID mapping.
async toolFunction({ realm, journeyName, description, identityResource, journeyData }: { realm: string; journeyName: string; description?: string; identityResource?: string; journeyData: JourneyInput; }) { try { // Step 1: Validate connection targets const validation = validateConnectionTargets(journeyData); if (!validation.isValid) { return createToolResponse(`Invalid journey structure: ${validation.errors.join('; ')}`); } // Step 2: Generate ID mapping (includes PageNode child IDs) const idMapping = generateNodeIdMapping(journeyData); // Step 3: Transform journey data const transformedJourney = transformJourneyIds(journeyName, journeyData, idMapping); // Step 4: Build API payload const payload = { ...transformedJourney, ...(description && { description }), ...(identityResource && { identityResource }) }; // Step 5: Make API call const url = buildAMJourneyUrl(realm, journeyName); const { response } = await makeAuthenticatedRequest(url, SCOPES, { method: 'PUT', headers: AM_API_HEADERS, body: JSON.stringify(payload) }); // Step 6: Return result with ID mapping const result = { success: true, journeyName, nodeIdMapping: idMapping }; return createToolResponse(formatSuccess(result, response)); } catch (error: any) { const category = categorizeError(error.message); return createToolResponse(`Failed to create journey "${journeyName}" [${category}]: ${error.message}`); } } }; - src/tools/am/createJourney.ts:28-63 (schema)Input schema for createJourney using Zod, defining realm, journeyName, description, identityResource, and journeyData (with entryNodeId, nodes including nodeType/displayName/connections/config).
inputSchema: { realm: z.enum(REALMS).describe('The realm to create the journey in'), journeyName: safePathSegmentSchema.describe('The name of the journey'), description: z.string().optional().describe('Admin-facing description of the journey'), identityResource: z .string() .optional() .describe( 'The identity resource that the journey authenticates against. Expected format: "managed/<realm>_<objectType>" (e.g., "managed/alpha_user", "managed/bravo_role").' ), journeyData: z .object({ entryNodeId: z .string() .describe('ID of the first node (connected from Start). Can be human-readable; will be transformed to UUID.'), nodes: z .record( z.object({ nodeType: z.string().describe('The AM node type (e.g., "PageNode", "IdentityStoreDecisionNode")'), displayName: z.string().describe('Admin-facing display name for this node'), connections: z .record(z.string()) .describe('Map of outcome IDs to target node IDs. Use "success" or "failure" for terminal nodes.'), config: z .record(z.any()) .describe( 'Node-specific configuration. For PageNodes, include the "nodes" array with child node definitions.' ) }) ) .describe( 'Map of node IDs to node definitions. Keys can be human-readable (e.g., "login-page"); they will be transformed to UUIDs.' ) }) .describe('The journey structure') }, - src/utils/amHelpers.ts:87-100 (schema)JourneyInput interface used as the TypeScript type for the journeyData parameter of createJourney, with entryNodeId and nodes record.
export interface JourneyInput { entryNodeId: string; nodes: Record<string, JourneyNodeInput>; } /** * Transformed journey ready for AM API */ export interface TransformedJourney { _id: string; entryNodeId: string; nodes: Record<string, AMNode>; staticNodes: Record<string, object>; } - src/tools/am/index.ts:8-8 (registration)Export of createJourneyTool from the AM tools barrel file, making it available for registration via getAllTools().
export { createJourneyTool } from './createJourney.js'; - src/index.ts:27-44 (registration)Generic tool registration loop that registers all tools (including createJourney) with the MCP server using server.registerTool().
allTools.forEach((tool) => { const toolConfig: ToolConfig = { title: tool.title, description: tool.description }; // Only add inputSchema if it exists (some tools like getLogSources don't have one) if ('inputSchema' in tool && tool.inputSchema) { toolConfig.inputSchema = tool.inputSchema; } // Add annotations if present if ('annotations' in tool && tool.annotations) { toolConfig.annotations = tool.annotations; } server.registerTool(tool.name, toolConfig, tool.toolFunction as any); }); - src/utils/amHelpers.ts:411-548 (helper)Helper functions used by createJourney: generateNodeIdMapping (creates UUID mapping for human-readable node IDs), validateConnectionTargets (validates all connections reference valid nodes), and transformJourneyIds (transforms journey data to use UUIDs/static IDs).
export function generateNodeIdMapping(journeyData: JourneyInput): Record<string, string> { const idMapping: Record<string, string> = {}; // Process top-level nodes for (const nodeId of Object.keys(journeyData.nodes)) { idMapping[nodeId] = UUID_REGEX.test(nodeId) ? nodeId : randomUUID(); } // Process PageNode child nodes for (const node of Object.values(journeyData.nodes)) { if (node.nodeType === 'PageNode' && Array.isArray(node.config?.nodes)) { for (const childNode of node.config.nodes) { if (childNode._id && !idMapping[childNode._id]) { idMapping[childNode._id] = UUID_REGEX.test(childNode._id) ? childNode._id : randomUUID(); } } } } return idMapping; } /** * Validates that all connection targets reference valid nodes or aliases. * Also checks that no node connects to itself (self-reference). * * @param journeyData - The journey input data * @returns Object with isValid boolean and array of error messages */ export function validateConnectionTargets(journeyData: JourneyInput): { isValid: boolean; errors: string[]; } { const errors: string[] = []; const validTargets = new Set([...Object.keys(journeyData.nodes), ...Object.keys(CONNECTION_ALIASES)]); // Check entryNodeId if (!journeyData.nodes[journeyData.entryNodeId]) { errors.push(`entryNodeId "${journeyData.entryNodeId}" does not reference a valid node`); } // Check all connections for (const [nodeId, node] of Object.entries(journeyData.nodes)) { for (const [outcome, targetId] of Object.entries(node.connections)) { // Check for self-reference if (targetId === nodeId) { errors.push(`Node "${nodeId}" outcome "${outcome}" cannot connect to itself`); continue; } const lowerTarget = targetId.toLowerCase(); if (!validTargets.has(targetId) && !CONNECTION_ALIASES[lowerTarget]) { errors.push(`Node "${nodeId}" outcome "${outcome}" references unknown target "${targetId}"`); } } } return { isValid: errors.length === 0, errors }; } /** * Transforms a journey definition to use UUIDs for all node references. * * - Replaces node keys with UUIDs * - Updates entryNodeId * - Updates all connection target references * - Resolves "success"/"failure" aliases to static node IDs * - Sets config._id to match the node's UUID * - Transforms PageNode child node IDs * * @param journeyName - The name of the journey * @param journeyData - Original journey data with human-readable IDs * @param idMapping - Mapping from original IDs to UUIDs * @returns Transformed journey data ready for AM API */ export function transformJourneyIds( journeyName: string, journeyData: JourneyInput, idMapping: Record<string, string> ): TransformedJourney { const resolveId = (id: string): string => { // Check if it's an alias first (case-insensitive) const lowerCaseId = id.toLowerCase(); if (CONNECTION_ALIASES[lowerCaseId]) { return CONNECTION_ALIASES[lowerCaseId]; } // Then check the mapping if (idMapping[id]) { return idMapping[id]; } // If not found, return as-is (might be a real UUID already) return id; }; const transformedNodes: Record<string, AMNode> = {}; for (const [originalId, node] of Object.entries(journeyData.nodes)) { const newId = idMapping[originalId]; // Transform connections const transformedConnections: Record<string, string> = {}; for (const [outcome, targetId] of Object.entries(node.connections)) { transformedConnections[outcome] = resolveId(targetId); } // Transform PageNode child node IDs if present // Child nodes are internal to PageNode and not referenced elsewhere in the graph, // so we auto-generate UUIDs for any that don't have explicit _id values const transformedConfig = { ...node.config }; if (node.nodeType === 'PageNode' && Array.isArray(node.config?.nodes)) { transformedConfig.nodes = node.config.nodes.map((childNode: any) => ({ ...childNode, _id: childNode._id || randomUUID() })); } // Build transformed node transformedNodes[newId] = { nodeType: node.nodeType, displayName: node.displayName, connections: transformedConnections, config: { ...transformedConfig, _id: newId // Inject the UUID into config } }; } return { _id: journeyName, entryNodeId: resolveId(journeyData.entryNodeId), nodes: transformedNodes, staticNodes: buildStaticNodes() }; }