What
The workflow builder (ui/src/components/flow/AddNodePanel.tsx + ui/src/app/workflow/[workflowId]/hooks/useWorkflowState.ts) allows the user to add a second startCall node to a workflow and save it. The backend validator enforces "exactly one start" only at execute time, and the error is wrapped in a generic TextChatSessionExecutionError("Failed to execute text chat assistant turn") (api/services/workflow/text_chat_session_service.py:209), so when the user triggers a chat or call the failure message gives no hint about the real cause.
Concrete failure mode encountered: a Telegram bot voice/chat flow on a workflow with two starts returned Dograh API 500: {"detail":"Failed to execute text chat assistant turn"}. The actual cause — "Workflow has 2 start nodes — exactly one is required" — was only surfaceable by reading workflow_run_text_sessions.session_data directly.
Repro
- Open any workflow in the builder.
- Click "Add node" → Triggers → "Start Call". A second
startCall is silently added.
- Save (Cmd/Ctrl+S). Save succeeds without complaint.
- Trigger any execution (text chat in the agent tester, voice call via Telegram bot, etc.).
- Get the generic 500.
Why it happens
AddNodePanel has no conditional on existing nodes; the trigger button is always clickable.
saveWorkflow ships whatever's in the store; no client-side preflight for graph invariants.
- The execute-time validator's message is correct and specific, but the wrapper in
execute_pending_text_chat_turn drops it from the HTTP response detail.
Suggested fix
I've opened a PR on my fork doing two things — happy to upstream it if useful:
- AddNodePanel: disable any spec in the
trigger category when one already exists on the canvas, with a tooltip explaining why. Matches Activepieces' "one trigger, replace-in-place" pattern. (Zapier does the same; n8n is the multi-trigger outlier but their data model is built for it — Dograh's isn't.)
- saveWorkflow: belt-and-suspenders client-side preflight that toasts and refuses to save when
triggerCount > 1, so paste/import flows don't slip past.
A separate small fix is also worth doing on the API side: include the original exception message in TextChatSessionExecutionError so the 500 response detail is debuggable without DB access. (My fork has this as a separate PR — see stefandsl/dograh#25.)
Out of scope but worth thinking about
A "Replace trigger" affordance on the existing start node (Activepieces-style pencil icon) would let users swap trigger types without the "delete first, add second" two-step.
What
The workflow builder (
ui/src/components/flow/AddNodePanel.tsx+ui/src/app/workflow/[workflowId]/hooks/useWorkflowState.ts) allows the user to add a secondstartCallnode to a workflow and save it. The backend validator enforces "exactly one start" only at execute time, and the error is wrapped in a genericTextChatSessionExecutionError("Failed to execute text chat assistant turn")(api/services/workflow/text_chat_session_service.py:209), so when the user triggers a chat or call the failure message gives no hint about the real cause.Concrete failure mode encountered: a Telegram bot voice/chat flow on a workflow with two starts returned
Dograh API 500: {"detail":"Failed to execute text chat assistant turn"}. The actual cause —"Workflow has 2 start nodes — exactly one is required"— was only surfaceable by readingworkflow_run_text_sessions.session_datadirectly.Repro
startCallis silently added.Why it happens
AddNodePanelhas no conditional on existing nodes; the trigger button is always clickable.saveWorkflowships whatever's in the store; no client-side preflight for graph invariants.execute_pending_text_chat_turndrops it from the HTTP response detail.Suggested fix
I've opened a PR on my fork doing two things — happy to upstream it if useful:
triggercategory when one already exists on the canvas, with a tooltip explaining why. Matches Activepieces' "one trigger, replace-in-place" pattern. (Zapier does the same; n8n is the multi-trigger outlier but their data model is built for it — Dograh's isn't.)triggerCount > 1, so paste/import flows don't slip past.A separate small fix is also worth doing on the API side: include the original exception message in
TextChatSessionExecutionErrorso the 500 response detail is debuggable without DB access. (My fork has this as a separate PR — see stefandsl/dograh#25.)Out of scope but worth thinking about
A "Replace trigger" affordance on the existing start node (Activepieces-style pencil icon) would let users swap trigger types without the "delete first, add second" two-step.