Identity Orchestration · 14 min read

Flows and nodes

How IO flows are organised (types, categories, defaults, versions and library flows), how a journey is started, and which node to use for each job: pages, API calls, scripts, conditions, loops and Thales service nodes.

Flows, flow types and categories

A flow is a graph of connected nodes, stored as JSON. Every flow has a flow type, and every type belongs to a category. The type decides three things: the journey's URL, which nodes the designer offers, and which flow runs by default for that journey.

CategoryBuilt-in flow typesTypical journeys
Authentication (auth)login, logoutusername and password sign-in, sign-out
Self Service (self)registration, reset_pwd, update_pwdregistration, password reset and change
Admin (admin)invite_user, delete_user, lock_user, unlock_useroperator tasks on accounts
API (api)webhookflows called by another system, without a browser
Custom (custom)your own typesany journey you design: passkey sign-in, enrollment, profile pages
Library (type library, shown as Nestable in IO 6.0)libraryshared steps that other flows call; never run on their own
  • Give each new journey its own flow type in the custom category (Administration → Flow Categorisations → Flow Types). A type has one default flow, so a second journey in the same type could only run by replacing the first. Reuse a type only for a new version of the same journey.
  • Use a short lowercase type name with _, such as passkey_signin. It appears in the journey's URL.
  • You can't change a flow's type after you create it. To move a flow, create a new flow of the right type with the same content.
  • A flow type you save as not deletable can never be deleted. Leave Deletable on for test and demo types.
  • The category limits the node palette. For example, page nodes aren't offered in API flows, and backend start and end nodes only appear in custom and API flows.

How a journey is started

JourneyStarted byAddress
Sign-in for an applicationOneWelcome Access, when the application sends the user to sign inThe flow URL is set on the Access side. The flow starts with Flow - Start (Access). See Add sign-in to your application.
Browser journey (custom type)A link or redirecthttps://{tenant-host}/io/flow/custom/{flow-type}?domain_id={domain-id}&flow_id={flow-id}
Built-in browser journey (login, registration, …)A link or redirecthttps://{tenant-host}/io/flow/{category}/{flow-type}?domain_id={domain-id}&flow_id={flow-id}
API flow (type webhook)Another system, with POSThttps://{tenant-host}/io/flow/execute/api/webhook?domain_id={domain-id}&flow_id={flow-id}
Library flowAnother flow, through Flow - Execute Nested Flownone: a library has no URL
  • Copy Flow URL (flow list or designer settings menu) gives you the exact address. Copy ID gives the flow ID only.
  • Sign-in journeys start and end at Access. Flow - Start (Access) has the outputs Success, NoToken (someone opened the URL directly), InvalidToken (the hand-over from Access expired) and Failure. The success path ends at Flow - End Success (Access), which reports the signed-in user back to Access. NoToken and InvalidToken show a page that tells the user to start again from the application. IO can't restart that journey itself.
  • The headless execute URL only runs flows of type webhook. Other types answer Invalid flow type there. Don't add a trailing slash: the gateway rejects it.
  • A webhook flow returns the body and HTTP status you set on its Flow - End Success Backend or Flow - End Failure Backend node, as is.
  • If your link carries a # fragment, put another parameter after flow_id first (…&flow_id={flow-id}&src=app#…). Otherwise the fragment becomes part of the flow ID and the flow isn't found.
  • Between steps, the flow session ID travels as a bearer token by default. A flow type can send it in a header, cookie, body field or query parameter instead (flow type settings, Where to send the flow session). Decide this before you build a client for an API flow that pauses with Flow - Callback.

Active, default and versions

SettingWhat it doesWhere
ActiveOnly active flows run from their URL. Test an inactive flow with a Debug link.Pencil icon in the flow list
Default flowThe one flow the platform runs for that flow type. Setting a new default replaces the old one.Pencil icon in the flow list
Enable nodes trackingRecords each executed step: always, only when an error occurs, or only when the flow fails.Pencil icon, or Edit Flow in the designer
VersionsAdd New Version (empty, from a file, or a copy of the current content), Modify (edits the version only), Restore (replaces the live content with that version), and Copy Flow URL to test one version.Designer settings menu
  • Add a version before any significant change, with a name that says why (v2-add-otp). A version is visible only inside its own flow.
  • Restore changes the live flow immediately, like a save.
  • To delete a flow, make it inactive first. A flow that is the default for its type should get a replacement default before you remove it.
  • Export flows from the list's download icon or the designer's settings menu. IO 6.0 can also export several flows at once and include the library flows they use (Advanced on a flow list).

Nodes and outputs

Each node has a type, a title you choose, its settings, one input and named outputs. Some nodes run in the user's browser (pages, browser scripts); the rest run on the IO server. The family decides which outputs exist:

FamilyNodes (designer names)Outputs
Start and endFlow - Start (Access), Flow - Start Frontend, Flow - Start Backend, Flow - Start Login and the other per-type starts; the matching End Success and End Failure nodesStart nodes: Success or a type-specific set, plus Failure. End nodes have none.
PagesFrontend - Form (your HTML and script, or the form builder), Frontend - Script (browser code without a page, such as a passkey prompt)one output per submit key you declare, plus Failure
HTTP requestsBackend - HTTP Request (server side, may carry credentials), Frontend - HTTP Request (browser, never credentials)Success (2xx), Failure (anything else)
ScriptsBackend - Script, and the JS runner node where your tenant has itthe outputs your code declares, plus Failure
ConditionsCondition Wizard V2True, False, Failure
LoopsLoopHasNext (next item), Done, InvalidArray, Failure
VariablesSet Flow Constants, Flow Session - Update variables, Delete Object (where registered)Success, Failure (Delete Object adds ObjectNotFound)
Settings and secretsLoad From VaultSuccess, Failure
Library callsFlow - Execute Nested FlowNestedSuccess, NestedFailure, Failure, plus each custom outcome you declare
Thales service nodesIdentity Store, Password Store, Action Challenge, TOTP, Recovery Codes, Messaging, Consent, Identity Linking, Emit Event, Risktheir own sets, for example Success, MissingData, Failure, or Valid, Invalid, Failure
Sessions, tokens, federationCustom Session, User Session, JWT - Verifier, JWT Generator, OpenID and SAML identity providersper node

Connect every output. If an output fires and goes nowhere, the run stops with "<output>" output of node <id> is not connected to any node: an HTTP 500 for an API caller, a lost session for a browser user. Read a node's real outputs in the designer; don't assume they are only Success and Failure.

A Condition Wizard V2 node compares variables by bare path, without braces:

{ "string_filter": "(idsUser.data.totalElements gt 0)" }

A Loop walks an array. Connect HasNext to the loop body, and the body's last step back into the loop. Each item is available as {{item.element}}:

{ "array": "{{list.data.items}}", "is_array": true, "flow_variable": "item",
  "keys_to_store": ["index", "element"], "frontend_available": false }

Choosing a node: for a call to a platform service, use the Thales service node when your tenant has it registered. It gets its own token, calls the service and routes on the result. Otherwise use Backend - HTTP Request. Write a script only for logic no node covers. Backend scripts can't make HTTP calls themselves: make the call in an HTTP request node and process the result in the script.

Flow variables

Nodes store their results under a variable name in the node's Save in the flow store settings: the variable name, the keys to keep, and frontend available. Later nodes read them with templates:

{{login.username}}                 a field a form saved under "login"
{{idsUser.data.content.0.id}}      an array item, by index
{{flow.error.message}}             the last failed node's error (built in)
{{flow.flow_session}}              this run's session ID (built in)
  • Templates are replaced as text before the node runs, in pages, scripts, URLs and bodies, comments included. In scripts, quote strings ("{{login.username}}") and insert objects bare (var o = {{result.data}};).
  • Frontend available decides whether the browser can see a variable. Turn it off for passwords, tokens and anything from a vault. A variable stored with it off renders as undefined on pages and in end-node bodies, but works in backend nodes.
  • Use the same frontend setting on every node that writes the same variable name. Mixed settings create two copies that silently differ.
  • Setting a variable merges into its current value; it doesn't replace it. Keys the new value doesn't set keep their old values, such as an earlier error code. Before a step that can run twice (a retry, a refresh, a loop back to a form), clear the variable with Delete Object.
  • Don't use built-in names (flow, user, request) as your own variable names.
  • An unset variable can arrive as the literal {{…}}, as undefined or as null. Check for all three before you trust a value.

Calling platform APIs from a flow

Without a Thales service node, call a platform API in two Backend - HTTP Request nodes: get a client-credentials token, then make the call. The client ID, client secret and hosts come from vaults (see Vaults).

{ "method": "POST",
  "url": "{{esv.tokenUrl}}",
  "headers": { "Content-Type": "application/x-www-form-urlencoded" },
  "data": "grant_type=client_credentials&client_id={{esv.identityClientId}}&client_secret={{secrets.identityClientSecret}}",
  "flow_variable": "token", "keys_to_store": ["data"], "frontend_available": false }
{ "method": "GET",
  "url": "{{esv.gatewayUrl}}/identity-store/api/v1/identities",
  "headers": { "Authorization": "Bearer {{token.data.access_token}}" },
  "flow_variable": "result", "keys_to_store": ["data"], "frontend_available": false }
  • The token URL is https://{tenant-host}/oauth/v1/token. The client must have the service in its Default audiences and the operation's scope in its Default scopes: Access clients, Scopes, and each API's Connect section.
  • The response body is at {{result.data}}. On Failure the error response is stored in the same variable, so a condition can check it (for example an Identity Store IDS-409 conflict).
  • Get the token once per library or round of calls, never before every call. Cache it in a custom session and reuse it until it is about to expire.
  • Keep Skip certificate validation off on https calls that carry credentials. For a private certificate authority, use the node's CA Certificate field. Keep Follow Redirects off on token and credential calls.

Library flows (nested flows)

When two flows need the same steps, put the steps in a library flow and call it with Flow - Execute Nested Flow. Fix it once, and every journey gets the fix. Typical libraries: the page theme, load settings, get a service token, find or create a user, send an email.

Library end nodeOutput on the caller's nested flow node
Flow - Nested Start (entry; outputs Next, Failure)none
Flow - Nested SuccessNestedSuccess
Flow - Nested FailureNestedFailure
Flow - Nested Custom End with output name UserExistsUserExists, declared in the caller's Variable Output Table
(the nested run itself failed)Failure
{ "nested_flow_id": "{flow-id}",
  "decision_outputs": { "UserExists": "UserExists" } }
  • Variables are shared both ways. The library reads the caller's variables, and what it sets is visible to the caller afterwards. Give private working variables distinct names, so two libraries don't overwrite each other.
  • Declare the inputs a library expects (for example {{login.username}}) in a comment on its start node and in its description.
  • Each custom outcome name must match the Flow - Nested Custom End output name exactly, character for character. An undeclared outcome has no route.
  • A nested call always waits for the library to finish. There is no background mode.
  • Keep nesting to one or two levels. Deep chains of libraries are hard to debug and slow.
  • Libraries can contain pages and browser steps. They run in the caller's browser session.
  • When you copy flows to another environment, copy the libraries first, then point each caller's nested flow node at the library's new ID.

Related