Skip to main content

Sub-session Workflow and Team Assignment Design

Linear: IMP-1119, child of Snoonu IMP-700.

Goal

Let workflows create a depth-one sub-session and let agents choose or change a sub-session’s support team both during creation and directly from the parent session’s Sub-sessions card.

Existing behavior

  • A sub-session is a normal chat_sessions row with parent_session_id and parent_ticket_number.
  • The hierarchy is intentionally one level deep. Manual creation and retroactive linking lock the affected parent rows before writing so concurrent operations cannot create a depth-two tree.
  • Agent-created sub-sessions are supported only where InitiateSessionUtils.isSubSessionsEnabled is true: OpenCX ticketing and Zendesk v2.
  • Any opened sub-session can already use the normal header team switcher, and the existing POST /backend/chat-session/update/:sessionId/team route enforces sessions:write and the standard support-team assignment behavior.
  • The parent card renders each child inside a navigation link. Its embedded team badge is therefore intentionally non-interactive.
  • Workflows have no sub-session action. The closest action, Create Ticket, inserts a standalone row and does not enforce or emit sub-session behavior.

Design

Create Sub-session workflow action

Add WorkflowActionEnum.CREATE_SUB_SESSION = 'create-sub-session' and register a Create Sub-session non-terminal action in both eager and lazy registries. Inputs:
  • parentSessionNumber: number — required and resolved within ctx.organization.id.
  • team: string | undefined — optional support-team lazy select.
Outputs:
  • sessionId: string
  • sessionNumber: number
The action creates an open child without sending a message. It inherits the parent’s contact, channel, requester email, and source email address. It marks the session outbound, matching agent-created sub-sessions. If a team is supplied, the action validates that it belongs to the organization, is not deleted, and is support-enabled, then stores it as the child’s initial team while leaving the child unassigned. Before insertion, the action verifies that the organization’s ticketing integration supports sub-sessions and that the parent is a top-level session. The parent is locked and rechecked inside the insertion transaction, preserving the current concurrency invariant. Cross-organization session or team identifiers never resolve. After commit, the parent receives the existing SUB_SESSION_CREATED internal timeline event with the child identifiers plus workflow_id and workflow_run_id, and the parent is socket-refreshed. No new message enum or migration is needed.

Team during dashboard creation

Extend CreateAgentSessionDto with optional group_id. The agent-initiated creation service validates an explicitly supplied team before any session row or outbound side effect is created and stores it as the initial inbox_id. Omitting the field preserves existing behavior. The New Session dialog displays an optional Team select only when parentSessionId is present. It lists support-enabled teams, submits group_id for both existing- and new-contact branches, and resets the selection when the dialog closes. The current assignee selection remains authoritative; initial team selection does not invoke automatic distribution.

Inline team assignment from the parent card

The parent Sub-sessions card renders the existing SessionBadge__Team control outside the child’s navigation link, beside the unlink control. The embedded static team badge is hidden only when the external interactive control is shown, avoiding duplicate team labels. The inline control is gated by sessions:write, independently of sessions-sub-sessions:write: changing ownership is a normal session mutation, while linking and unlinking remain sub-session-management mutations. It calls the existing team update route and assignment service, then refreshes the Sub-sessions SWR query so the card immediately reflects the new team.

Error behavior

  • Missing or foreign parent session number: workflow action returns a runtime error and writes nothing.
  • Parent is already a sub-session: runtime error and no child.
  • Unsupported ticketing integration: runtime error and no child.
  • Missing, foreign, deleted, or non-support team: action/service error and no child.
  • No team supplied: child is created with inbox_id = null.
  • Inline team mutation without sessions:write: existing route returns 403.

Testing strategy

  • Real Postgres workflow-action tests cover the success shape, inherited fields, optional team, all validation failures, tenant isolation, and parent-link concurrency.
  • A real workflow-runner test executes the registered action and asserts step output, child linkage, and workflow-attributed timeline data.
  • Real agent-initiated service/controller tests cover initial team persistence, omission, invalid teams, and no-orphan guarantees.
  • A full-stack Playwright test uses development login plus real backend routes to create a parent, create a team-owned child through the dialog, and change that team inline from the parent card. It also checks that the standalone New Session dialog does not expose the sub-session-only Team field.
  • Focused typechecks, lint, formatting, SDK generation checks, and touched-area tests run before commit and PR creation.

Non-goals

  • No link/unlink workflow actions.
  • No nested sub-sessions.
  • No first-message composer inside the workflow action.
  • No new shared abstraction, database migration, or public API endpoint.
  • No change to automatic assignment strategy; workflows can follow the new action with the existing Assign Session to Support Team action when picker-based routing is desired.