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.
| Category | Built-in flow types | Typical journeys |
|---|---|---|
Authentication (auth) | login, logout | username and password sign-in, sign-out |
Self Service (self) | registration, reset_pwd, update_pwd | registration, password reset and change |
Admin (admin) | invite_user, delete_user, lock_user, unlock_user | operator tasks on accounts |
API (api) | webhook | flows called by another system, without a browser |
Custom (custom) | your own types | any journey you design: passkey sign-in, enrollment, profile pages |
Library (type library, shown as Nestable in IO 6.0) | library | shared steps that other flows call; never run on their own |
- Give each new journey its own flow type in the
customcategory (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 aspasskey_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
| Journey | Started by | Address |
|---|---|---|
| Sign-in for an application | OneWelcome Access, when the application sends the user to sign in | The 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 redirect | https://{tenant-host}/io/flow/custom/{flow-type}?domain_id={domain-id}&flow_id={flow-id} |
| Built-in browser journey (login, registration, …) | A link or redirect | https://{tenant-host}/io/flow/{category}/{flow-type}?domain_id={domain-id}&flow_id={flow-id} |
API flow (type webhook) | Another system, with POST | https://{tenant-host}/io/flow/execute/api/webhook?domain_id={domain-id}&flow_id={flow-id} |
| Library flow | Another flow, through Flow - Execute Nested Flow | none: 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) andFailure. The success path ends at Flow - End Success (Access), which reports the signed-in user back to Access.NoTokenandInvalidTokenshow 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 answerInvalid flow typethere. Don't add a trailing slash: the gateway rejects it. - A
webhookflow 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 afterflow_idfirst (…&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
| Setting | What it does | Where |
|---|---|---|
| Active | Only active flows run from their URL. Test an inactive flow with a Debug link. | Pencil icon in the flow list |
| Default flow | The 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 tracking | Records each executed step: always, only when an error occurs, or only when the flow fails. | Pencil icon, or Edit Flow in the designer |
| Versions | Add 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:
| Family | Nodes (designer names) | Outputs |
|---|---|---|
| Start and end | Flow - Start (Access), Flow - Start Frontend, Flow - Start Backend, Flow - Start Login and the other per-type starts; the matching End Success and End Failure nodes | Start nodes: Success or a type-specific set, plus Failure. End nodes have none. |
| Pages | Frontend - 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 requests | Backend - HTTP Request (server side, may carry credentials), Frontend - HTTP Request (browser, never credentials) | Success (2xx), Failure (anything else) |
| Scripts | Backend - Script, and the JS runner node where your tenant has it | the outputs your code declares, plus Failure |
| Conditions | Condition Wizard V2 | True, False, Failure |
| Loops | Loop | HasNext (next item), Done, InvalidArray, Failure |
| Variables | Set Flow Constants, Flow Session - Update variables, Delete Object (where registered) | Success, Failure (Delete Object adds ObjectNotFound) |
| Settings and secrets | Load From Vault | Success, Failure |
| Library calls | Flow - Execute Nested Flow | NestedSuccess, NestedFailure, Failure, plus each custom outcome you declare |
| Thales service nodes | Identity Store, Password Store, Action Challenge, TOTP, Recovery Codes, Messaging, Consent, Identity Linking, Emit Event, Risk | their own sets, for example Success, MissingData, Failure, or Valid, Invalid, Failure |
| Sessions, tokens, federation | Custom Session, User Session, JWT - Verifier, JWT Generator, OpenID and SAML identity providers | per 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
undefinedon 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
{{…}}, asundefinedor asnull. 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}}. OnFailurethe error response is stored in the same variable, so a condition can check it (for example an Identity StoreIDS-409conflict). - 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 node | Output on the caller's nested flow node |
|---|---|
Flow - Nested Start (entry; outputs Next, Failure) | none |
| Flow - Nested Success | NestedSuccess |
| Flow - Nested Failure | NestedFailure |
Flow - Nested Custom End with output name UserExists | UserExists, 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.