Embed OU tree data in flow responses for unauthenticated OU selection #2160
Replies: 5 comments
-
Related DiscussionThis builds on the architecture established in #1816 (Interactive OU Selection via OUResolver Prompt Strategy). The core interaction model is the same - stateless executor, This discussion extends that design with:
|
Beta Was this translation helpful? Give feedback.
-
|
Can you give the request response format of the API call between the client and server when expanding the node and load |
Beta Was this translation helpful? Give feedback.
-
|
Rather than introducing a new {
"flowId": "abc-123",
"flowStatus": "INCOMPLETE",
"type": "VIEW",
"data": {
"inputs": [
{ "identifier": "ouId", "type": "OU_SELECT", "required": true }
],
"actions": [
{ "ref": "action_submit", "nextNode": "ou_selection" }
]
"additionalData": {
"treeData": {
"parentId": "",
"nodes": [
{ "value": "root-id", "label": "Root", "hasChildren": true },
{ "value": "default-id", "label": "Default", "hasChildren": false }
],
"totalResults": 2,
"offset": 0,
"limit": 20
},
},
}
} |
Beta Was this translation helpful? Give feedback.
-
|
@senthalan Here are the request/response formats for each interaction type. Following @ThaminduDilshan's suggestion, 1. Initial page load (root nodes returned)Request: POST /flows/{flowId}{
"actionRef": "action_submit",
"inputs": {}
}Response: {
"flowId": "abc-123",
"flowStatus": "INCOMPLETE",
"type": "VIEW",
"data": {
"inputs": [
{ "identifier": "ouId", "type": "OU_SELECT", "required": true }
],
"actions": [
{ "ref": "action_submit", "nextNode": "ou_selection" }
],
"additionalData": {
"treeData": "{\"parentId\":\"\",\"nodes\":[{\"value\":\"root-id\",\"label\":\"Root\",\"hasChildren\":true},{\"value\":\"default-id\",\"label\":\"Default\",\"hasChildren\":false}],\"totalResults\":2,\"offset\":0,\"limit\":20}"
}
}
}Decoded {
"parentId": "",
"nodes": [
{ "value": "root-id", "label": "Root", "hasChildren": true },
{ "value": "default-id", "label": "Default", "hasChildren": false }
],
"totalResults": 2,
"offset": 0,
"limit": 20
}2. Expand a nodeRequest: {
"actionRef": "action_submit",
"inputs": {
"expandOuId": "root-id"
}
}Response: {
"flowId": "abc-123",
"flowStatus": "INCOMPLETE",
"type": "VIEW",
"data": {
"inputs": [
{ "identifier": "ouId", "type": "OU_SELECT", "required": true }
],
"actions": [
{ "ref": "action_submit", "nextNode": "ou_selection" }
],
"additionalData": {
"treeData": "{\"parentId\":\"root-id\",\"nodes\":[{\"value\":\"engineering-id\",\"label\":\"Engineering\",\"hasChildren\":true},{\"value\":\"marketing-id\",\"label\":\"Marketing\",\"hasChildren\":false},{\"value\":\"sales-id\",\"label\":\"Sales\",\"hasChildren\":true}],\"totalResults\":3,\"offset\":0,\"limit\":20}"
}
}
}Decoded {
"parentId": "root-id",
"nodes": [
{ "value": "engineering-id", "label": "Engineering", "hasChildren": true },
{ "value": "marketing-id", "label": "Marketing", "hasChildren": false },
{ "value": "sales-id", "label": "Sales", "hasChildren": true }
],
"totalResults": 3,
"offset": 0,
"limit": 20
}3. Load more children under a node (paginate)Request: {
"actionRef": "action_submit",
"inputs": {
"expandOuId": "engineering-id",
"offset": "20"
}
}Response: {
"flowId": "abc-123",
"flowStatus": "INCOMPLETE",
"type": "VIEW",
"data": {
"inputs": [
{ "identifier": "ouId", "type": "OU_SELECT", "required": true }
],
"actions": [
{ "ref": "action_submit", "nextNode": "ou_selection" }
],
"additionalData": {
"treeData": "{\"parentId\":\"engineering-id\",\"nodes\":[{\"value\":\"platform-id\",\"label\":\"Platform\",\"hasChildren\":false},{\"value\":\"devops-id\",\"label\":\"DevOps\",\"hasChildren\":false}],\"totalResults\":22,\"offset\":20,\"limit\":20}"
}
}
}Decoded {
"parentId": "engineering-id",
"nodes": [
{ "value": "platform-id", "label": "Platform", "hasChildren": false },
{ "value": "devops-id", "label": "DevOps", "hasChildren": false }
],
"totalResults": 22,
"offset": 20,
"limit": 20
}4. Final selection (flow advances)Request: {
"actionRef": "action_submit",
"inputs": {
"ouId": "engineering-id"
}
}Response (flow moves to the next step): {
"flowId": "abc-123",
"flowStatus": "INCOMPLETE",
"type": "VIEW",
"data": {
"inputs": [
{ "identifier": "username", "type": "TEXT_INPUT", "required": true }
],
"actions": [
{ "ref": "action_submit", "nextNode": "next_step" }
]
}
}Note on
|
Beta Was this translation helpful? Give feedback.
-
|
Taking a step back and challenging this requirement. Will we have the usecase to render the OU tree within the unauthentication flow? There will be definitely usecase to have an OU selector from a list of OUs, but I don't think of a usecase where the user selects the OU from an OU tree in an unauthenticated flow (self registration) @darshanasbg @ThaminduDilshan WDYT? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Problem Summary
The OU tree picker in onboarding flows currently fetches tree data by calling OU REST APIs (
GET /organization-units,GET /organization-units/{id}/children) directly from the frontend. This works in the admin console because the caller is authenticated, but it creates a hard dependency on API access. If we want to support OU selection in unauthenticated contexts - such as self-registration flows where a user picks their OU - the current approach breaks because the caller has no bearer token to call the OU APIs.More broadly, the OU tree picker is the only flow input that requires out-of-band API calls to populate its data. Every other input type (
SELECT,TEXT_INPUT,PASSWORD_INPUT, etc.) is self-contained in the flow response. This inconsistency limits where OU selection can be used and ties the frontend to a specific data-fetching strategy.Goal
Make OU tree data available through the flow response itself, so the frontend can render the OU tree picker without calling OU APIs separately. The flow engine becomes the single data source for all inputs, including hierarchical tree selection.
Constraints
RuntimeDataacross interactions - this would grow unboundedly as the user expands nodes.ExecUserInputRequiredloop - the executor returns data, the client responds, the executor runs again. No new flow engine concepts.OU_SELECTinput type and API-based tree picker in the admin console must continue to work unchanged.High-Level Approach
Introduce a stateless, lazy-loaded tree pattern where:
ExecUserInputRequired→ submit → re-execute loop that every other multi-step executor uses.GetOrganizationUnitChildrencall.This mirrors how the existing
OrganizationUnitTreePickeralready works (it fetches one level at a time via API calls and assembles the tree in React state), but replaces the API calls with flow interactions.Architecture Overview
Current: API-driven tree picker
The tree picker and the flow are separate systems. The picker uses authenticated API calls; the flow only receives the final selection.
Proposed: flow-embedded tree
The tree picker gets data through the flow. Every expand/paginate is a flow interaction - the executor fetches one page from the OU service and returns it. No separate API calls needed. Works authenticated or unauthenticated.
Interaction lifecycle within the flow
The flow stays on the same node throughout all expand/paginate interactions. Only the final
ouIdsubmission causes the flow to advance. This is the same pattern asSMSOTPAuthExecutor(send → verify) orBasicAuthExecutor(prompt → validate → retry).Why stateless?
If the executor accumulated the expanded tree server-side (in
RuntimeData), the state would grow with every expand - a user browsing a large tree could cause unbounded growth. Instead:Flow Response Format
Extended
OU_SELECTwith tree dataThe existing
OU_SELECTinput type is extended. When the executor includestreeDatain the response, the frontend renders the flow-based tree picker. WhentreeDatais absent, the frontend falls back to the existing API-based picker.Tree data in the response
A new
treeDatafield is added to theFlowDataresponse model (alongside existinginputs,actions,meta, andadditionalData). It carries one page of tree nodes:{ "flowId": "abc-123", "flowStatus": "INCOMPLETE", "type": "VIEW", "data": { "inputs": [ { "identifier": "ouId", "type": "OU_SELECT", "required": true } ], "treeData": { "parentId": "", "nodes": [ { "value": "root-id", "label": "Root", "hasChildren": true }, { "value": "default-id", "label": "Default", "hasChildren": false } ], "totalResults": 2, "offset": 0, "limit": 20 }, "actions": [ { "ref": "action_submit", "nextNode": "ou_selection" } ] } }parentIdnodesnodes[].valuenodes[].labelnodes[].hasChildrentotalResultsparentId(for pagination).offset/limitClient-to-server interactions
{ "expandOuId": "node-id" }{ "expandOuId": "node-id", "offset": "20" }{ "ouId": "selected-id" }ExecComplete, flow advancesAll interactions route back to the same executor node (via the prompt's
nextNode). The executor distinguishes them by which input keys are present.hasChildren- Avoiding N+1 QueriesTo populate
hasChildrenfor each node, the executor needs to know whether each child OU has its own children. Naive approach: callGetOrganizationUnitChildren(childId, 1, 0)for each child - this is N+1 queries per page.Better approaches:
Option A: Batch check - New service method
HasChildrenBatch(ctx, ids []string) (map[string]bool, error)that runs a single query:Option B: Include child count in list query - Modify
GetOrganizationUnitChildrento return achildCountfield via a subquery or window function, sohasChildren = childCount > 0with no extra round trip.Option B is preferable as it requires no additional queries and the information is derived in the same list query.
Scope and Backward Compatibility
OU_SELECTinput typetreeDatais present, uses flow-based picker; otherwise falls back to API-based pickerpromptstrategypromptAllstrategycallerstrategyOrganizationUnitTreePickerkeeps using API callstreeDatafrom flow responseThe executor strategy (
promptorpromptAll) determines whether tree data is included. Existing flows that rely on the API-based picker continue to work - the frontend only switches to the flow-based picker whentreeDatais present in the response.Performance Considerations
hasChildrensubquery (Option B), each expand/paginate is exactly one SQL query. No N+1 problem.ouIdis written to RuntimeData on final selection.Security Considerations
OU_SELECTwith tree data in registration flows if they intentionally want users to pick their OU. Thecallerstrategy remains available for restricted scenarios.ouIdselection is still validated by the executor (IsOrganizationUnitExistsorIsParent), anduser/service.go:validateOrganizationUnitForUserType()enforces OU-schema compatibility at user creation.Alternatives Considered
Alternative 1: Send the full OU tree in a single response
Alternative 2: Accumulate expanded tree state in executor RuntimeData
Alternative 3: Proxy OU APIs through a special flow endpoint (e.g.,
/flows/{flowId}/resources/ous)Alternative 4: Embed tree data in
AdditionalDataas a JSON stringAdditionalDataismap[string]stringtreeDatafield is cleaner and aligns with howinputs,actions, andmetaare structured.Design Decisions
1. Should
OU_TREE_SELECTbe a new input type, or shouldOU_SELECTgain tree data capabilities?Extend the existing
OU_SELECTinput type. WhentreeDatais present in the flow response, the frontend uses the flow-based tree picker; when absent, it falls back to the existing API-based picker. This avoids having two input types for the same concept and keeps backward compatibility - existing flows withouttreeDatawork exactly as before.2. Should the
promptstrategy also support flow-embedded tree, or onlypromptAll?Both
promptandpromptAllshould support flow-embedded tree. Thepromptstrategy scopes to a subtree (rooted at the user type's default OU), andpromptAllshows the full tree from root. Both return tree data in the flow response - the only difference is the root node the executor starts from. This ensures OU selection works in unauthenticated contexts regardless of the strategy used.3. What should the default page size be for tree node fetches?
Page size is configurable as an executor property (
pageSize), with a sensible default fallback (e.g., 20). This keeps simple flows simple while allowing tuning for deployments with large OU trees.4. Should expand interactions use a new action type, or reuse the existing action ref?
Expand and select use the same action ref, distinguished by input keys. The tree picker submits all interactions through a single action. The executor checks which input key is present:
expandOuId(with optionaloffset) triggers tree browsing, whileouIdtriggers final selection. This keeps the PROMPT node simple (one action) and the frontend doesn't need to map UI events to different action refs - it just submits different input payloads through the samesubmitFlowInput()call.5. How should
hasChildrenbe determined - batch check (Option A) or subquery (Option B)?Option B (subquery) is preferred. Including a correlated
EXISTSsubquery in the list query deriveshasChildrenwith no extra round trip. The added query complexity is minimal compared to the cost of a separate batch call (Option A), and it keeps the data-fetching path as a single query per interaction.Tracking issue: #2161
Beta Was this translation helpful? Give feedback.
All reactions