---
title: "Update Flow Automation"
method: PUT
path: "/v1/flow_automation/{flow_automation_id}/update"
tags: ["flow_automation"]
---

# Update Flow Automation

`PUT /v1/flow_automation/{flow_automation_id}/update`

## Request

- Base URL: `https://api.linktodm.com` (the document declares no server; this is the origin it was published from)
- URL: `https://api.linktodm.com/v1/flow_automation/{flow_automation_id}/update`
- Auth: HTTP bearer

Full update of a flow automation.

## Path parameters

- `flow_automation_id` integer, required

## Request body

- FlowAutomationUpdatePayload: Payload for updating a flow automation.
  - `is_active` boolean
  - `is_deleted` boolean
  - `name` string, nullable
  - `is_draft` boolean
  - `nodes` union[]
    - union
      - TriggerGroupFlowNode: The single root node of every flow. Acts as a container for all trigger nodes (which set it as their `parentId`). Every flow MUST have exactly one of these, with `data={}`. Outgoing edges from this node go to the first action node that runs after any trigger fires.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'TRIGGER_GROUP'
        - `data` TriggerGroupNodeData, required: Data for the single TRIGGER_GROUP node at the root of every flow. Has no fields of its own; exists to group all trigger nodes as children via their `parentId`. Use `{}` for this field.
      - UserCommentTriggerFlowNode: Trigger node: fires when someone comments on a post/reel/story. Must be a child of TRIGGER_GROUP (set `parentId` to the trigger group's id). One flow can have multiple comment triggers (e.g. different posts).
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'USER_COMMENT_TRIGGER'
        - `data` UserCommentTriggerData, required: Trigger: fires when a user comments on an Instagram post/reel/story (or a Facebook Page post). The most common entry point for comment-to-DM automations.
          - `one_trigger_per_user` boolean, nullable: If True, this trigger fires at most once per contact within `one_trigger_per_user_window_hours`, even if they comment, message or share again. A skipped event sends nothing: no DM flow and no public auto-reply. Null/False = fire on every matching event.
          - `one_trigger_per_user_window_hours` integer, nullable: Length of the per-contact window in hours. Null = 24. Max 168 (7 days) -- enforced on the create/update payload, deliberately NOT here (same read-path reason as `ai_prompt`).
          - `media_selection_type` 'SPECIFIC' | 'ALL' | 'NEXT', required: Media selection type for triggers.
          - `media_id` string, nullable: Instagram/Facebook media ID. Required when `media_selection_type=SPECIFIC`. Server fills this in for NEXT.
          - `media_url` string, nullable: Permalink to the post; informational, used for UI display.
          - `include_keywords` string[]: Comments matching any of these keywords trigger the flow. Ignored when `trigger_type=ANY`. Case-insensitive.
          - `exclude_keywords` string[]: Comments matching any of these keywords are skipped, even if include_keywords match.
          - `trigger_type` 'ANY' | 'SPECIFIC_KEYWORDS' | 'AI', required: Trigger type for media-based automations.
          - `ai_prompt` string, nullable: Instruction describing which comments/replies should fire the flow. Required when `trigger_type=AI`; ignored otherwise. Keep it to 100 characters or less -- that limit is enforced on the create/update payload, deliberately NOT here (see below).
          - `is_story` boolean: True if this trigger watches story replies/comments instead of feed posts. Stories use a separate webhook channel.
          - `enable_auto_reply` boolean: If True, the bot also publicly replies to the comment in addition to sending the DM flow. Used to nudge users to check their DMs.
          - `auto_reply_mode` 'MANUAL' | 'AI'
          - `auto_reply_messages` string[], nullable: Pool of public reply texts; one is picked at random per match. Required when `enable_auto_reply=True` and `auto_reply_mode=MANUAL`.
          - `auto_reply_ai_prompt` string, nullable: Instructions passed to the LLM to generate the public reply. Required when `enable_auto_reply=True` and `auto_reply_mode=AI`.
          - `auto_reply_limit` integer, nullable: Max public auto-replies per day on this post. Null = unlimited. Use to avoid spamming the comment section.
          - `auto_reply_delay_in_seconds` integer, nullable: Wait this long before posting the public reply; the DM flow still runs immediately. Null = reply straight away. Scheduled as a delayed Cloud Task, same as a basic automation's comment step.
          - `auto_reply_delay_unit` 'SECONDS' | 'MINUTES' | 'HOURS' | 'DAYS': Unit a user entered a wait duration in. Shared by the legacy automation step delays (`StepDelay.delay_unit`) and the flow builder's DELAY node / comment auto-reply delay. The backend always schedules with the canonical seconds value; this is what the UI round-trips so the editor can show the delay the way it was typed.
      - UserSendsMessageTriggerFlowNode: Trigger node: fires when someone sends a DM to the account. Must be a child of TRIGGER_GROUP.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'USER_SENDS_MESSAGE'
        - `data` UserSendsMessageTriggerData, required: Trigger: fires when a user sends a direct message (DM) to the Instagram/Facebook account. Use this for inbox automations and keyword-based DM responses.
          - `one_trigger_per_user` boolean, nullable: If True, this trigger fires at most once per contact within `one_trigger_per_user_window_hours`, even if they comment, message or share again. A skipped event sends nothing: no DM flow and no public auto-reply. Null/False = fire on every matching event.
          - `one_trigger_per_user_window_hours` integer, nullable: Length of the per-contact window in hours. Null = 24. Max 168 (7 days) -- enforced on the create/update payload, deliberately NOT here (same read-path reason as `ai_prompt`).
          - `trigger_type` 'ANY' | 'SPECIFIC_KEYWORDS' | 'AI', required: Trigger type for media-based automations.
          - `keywords_checks` KeywordsCheck[], required: List of keyword rules; the trigger fires if ANY rule matches. Required when `trigger_type=SPECIFIC_KEYWORDS`. Empty otherwise.
            - `type` 'EQUALS' | 'CONTAINS' | 'NOT_CONTAINS' | 'BEGINS_WITH' | 'ENDS_WITH' | 'CONTAINS_WHOLE_WORD' | 'CONTAINS_WHOLE_PHRASE' | 'GREATER_THAN' | 'LESS_THAN' | 'HAS_VALUE' | 'NOT_EQUALS' | 'NOT_HAS_VALUE', required: Keyword check type for user sends message trigger.
            - `keywords` string[], required: List of keywords/phrases to test against the message. Matching is case-insensitive. Leave empty only for HAS_VALUE / NOT_HAS_VALUE.
          - `ai_prompt` string, nullable: Instruction describing which DMs should fire the flow. Required when `trigger_type=AI`; ignored otherwise. Keep it to 100 characters or less -- enforced on the create/update payload, not here (same read-path reason as UserCommentTriggerData.ai_prompt).
      - UserSharePostTriggerFlowNode: Trigger node: fires when someone shares a post/reel to your DMs. Must be a child of TRIGGER_GROUP.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'USER_SHARE_POST'
        - `data` UserSharePostTriggerData, required: Trigger: fires when a user shares a post/reel to your DMs (via the Instagram share button on a post). Used for 'share to get the link' style flows.
          - `one_trigger_per_user` boolean, nullable: If True, this trigger fires at most once per contact within `one_trigger_per_user_window_hours`, even if they comment, message or share again. A skipped event sends nothing: no DM flow and no public auto-reply. Null/False = fire on every matching event.
          - `one_trigger_per_user_window_hours` integer, nullable: Length of the per-contact window in hours. Null = 24. Max 168 (7 days) -- enforced on the create/update payload, deliberately NOT here (same read-path reason as `ai_prompt`).
          - `media_selection_type` 'SPECIFIC' | 'ALL' | 'NEXT', required: Media selection type for triggers.
          - `media_id` string, nullable: Required when `media_selection_type=SPECIFIC`.
          - `media_url` string, nullable: Permalink, for UI display.
      - UserStoryMentionTriggerFlowNode: Trigger node: fires when someone @mentions the account in their Instagram story. Must be a child of TRIGGER_GROUP.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'USER_STORY_MENTION'
        - `data` UserStoryMentionTriggerData, required: Trigger: fires when a user mentions your account in their Instagram story. No filter fields — every mention triggers.
      - PlainTextMessageFlowNode: Sends a plain text DM. The most common message node — use this for any text-only reply. Connect outgoing edge to the next step.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'PLAIN_TEXT_MESSAGE'
        - `data` PlainTextNodeData, required: Action: send a plain text DM to the user.
          - `message` string, required: DM body text. Supports template variables that are replaced at send time with the contact's values: `{{username}}`, `{{email}}`, `{{phone}}`, `{{instagram_user_id}}`. Keep under Meta's 1000-char limit for DM text.
      - ButtonTextMessageFlowNode: Sends a text DM with 1-3 attached buttons. Add the buttons as MESSAGE_BUTTON child nodes whose `parentId` equals this node's id. Without child MESSAGE_BUTTONs this renders as a plain text message.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'BUTTON_TEXT_MESSAGE'
        - `data` ButtonTextNodeData, required: Action: send a text message that has clickable buttons attached. Define the buttons as `MESSAGE_BUTTON` child nodes whose `parentId` is this node's id. Meta limit: max 3 buttons per text message.
          - `message` string, required: Text shown above the buttons. Supports `{{username}}`, `{{email}}`, `{{phone}}`, `{{instagram_user_id}}`.
      - ButtonTextImageMessageFlowNode: Sends a single image card (image + title + subtitle) with attached buttons. Use for promo cards, single-product showcases. For multiple cards, use CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE instead.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'BUTTON_TEXT_IMAGE_MESSAGE'
        - `data` ButtonTextImageNodeData, required: Action: send a card with image + title + subtitle + clickable buttons. Used either as a standalone message or as one item inside a carousel. Buttons are `MESSAGE_BUTTON` child nodes whose `parentId` is this node (or, when inside a carousel, use `carousel_item_index`).
          - `imageUrl` string, nullable: Public HTTPS URL of the card image (JPEG/PNG, <8MB).
          - `title` string, required: Card headline (max ~80 chars). Bold in the rendered card.
          - `subtitle` string, nullable: Card body text (max ~80 chars). Optional but recommended.
          - `url` string, nullable: If the user taps the image (not a button), open this URL. Leave null to make the image non-tappable.
      - CarouselButtonTextImageMessageFlowNode: Sends a horizontally-scrollable carousel of 2-10 image cards. Each card has its own buttons (MESSAGE_BUTTON child nodes with this node as `parentId` and the right `carousel_item_index`).
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE'
        - `data` CarouselButtonTextImageNodeData, required: Action: send a horizontally-scrollable carousel of image cards. Meta limit: 2-10 cards. Each card's buttons are MESSAGE_BUTTON nodes with this carousel as `parentId` and `carousel_item_index` set.
          - `items` ButtonTextImageNodeData[], required: The cards in the carousel, left-to-right. 2-10 items.
            - `imageUrl` string, nullable: Public HTTPS URL of the card image (JPEG/PNG, <8MB).
            - `title` string, required: Card headline (max ~80 chars). Bold in the rendered card.
            - `subtitle` string, nullable: Card body text (max ~80 chars). Optional but recommended.
            - `url` string, nullable: If the user taps the image (not a button), open this URL. Leave null to make the image non-tappable.
      - AttachmentMessageFlowNode: Sends one previously-uploaded attachment (image, video, audio or PDF) as its own DM. Carries no text — pair it with a PLAIN_TEXT_MESSAGE sibling if the file needs a caption. The file must already exist in the attachment library; this node only references it by id.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'ATTACHMENT_MESSAGE'
        - `data` AttachmentMessageNodeData, required: Action: send a stored media/document attachment as its own DM. The file is not carried here. It is uploaded ahead of time through `POST /v1/attachment/upload`, which registers it with Meta as a reusable attachment and returns a row id — that id is all the send path needs. At send time the row is looked up and its `meta_attachment_id` goes out in the message payload, exactly as the DM automation does for its Attachment type.
          - `attachment_id` integer, nullable: Row id from `POST /v1/attachment/upload` (NOT Meta's attachment_id — that is resolved server-side at send time). Null means the editor added the block but nothing has been uploaded into it yet; such a node sends nothing.
          - `attachment_data` AttachmentNodeSnapshot: The editor's cached copy of an Attachment row, parked on the node. Deliberately NOT `attachment.schemas.AttachmentItem`, even though it describes the same row. `AttachmentItem` makes `id`, `meta_attachment_id`, `file_url`, `type` and `created_at` required, which is right for an API response but wrong here: `FlowGraph._convert_nodes` re-validates every STORED node through `FlowNodeAdapter` on read, on ~14 paths including flow execution. A required field would therefore reject an already-saved node at read time and silently take a live automation offline (the same trap documented on `UserCommentTriggerData.ai_prompt` above). So every field is optional and `extra="allow"` keeps anything the editor adds later. This is a display cache: nothing here is trusted at send time, where the row is re-read from the database by `attachment_id`.
            - `id` integer, nullable: Attachment row id.
            - `meta_attachment_id` string, nullable: Meta's reusable attachment id. Mirrored for display only — the send path re-reads it from the database rather than trusting this copy, because it is account-scoped and a flow copied to another channel carries a stale one.
            - `file_url` string, nullable: Public URL of the stored file; drives the canvas preview.
            - `type` 'image' | 'video' | 'audio' | 'file'
            - `filename` string, nullable
            - `content_type` string, nullable
            - `file_size` integer, nullable
            - `created_at` string, nullable: ISO-8601 timestamp as the editor sent it. Deliberately a plain string, NOT a datetime: node data is persisted with `node.model_dump()` (python mode, see flow_automation/services.py) straight into the `nodes` JSON column, and a real datetime object is not JSON-serializable — typing this as `datetime` breaks every save of a flow containing an attachment. Nothing server-side does date arithmetic on it; it exists so the editor can show 'uploaded 3 days ago' without a refetch.
      - MessageGroupFlowNode: Visual container that groups a sequence of message nodes in the editor. No execution effect — purely organizational. Use sparingly.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'MESSAGE_GROUP'
        - `data` MessageGroupNodeData, required: Container node that visually groups a chain of message nodes in the editor. Has no fields and no execution semantics. Use `{}`.
      - MessageButtonFlowNode: A clickable button attached to a message. NEVER a standalone node: must have `parentId` set to a BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE node. Outgoing edges from this button represent what happens when the user taps it (the next flow step).
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'MESSAGE_BUTTON'
        - `data` MessageButtonNodeData, required: Action: a single clickable button attached to a text/image/carousel message. Parent it (via `parentId`) to the message node it belongs to.
          - `label` string, required: Button label shown to the user. Meta hard limit: 20 characters. Keep it short and action-oriented.
          - `action` ButtonAction: What a button does when tapped.
            - `type` 'OPEN_WEBSITE' | 'NONE': What happens when a contact taps a MESSAGE_BUTTON. OPEN_WEBSITE: opens `url` in browser; can carry click tracking. NONE: button continues the flow (next connected node fires) without leaving the conversation.
            - `url` string, nullable: Required when `type=OPEN_WEBSITE`. Must be HTTPS.
            - `additionalActions` boolean, nullable: If True, also continue the flow after opening the URL.
            - `disable_url_tracking` boolean, nullable: If True, the URL is sent unchanged (no Salesmorph click-tracking wrapper). Use for landing pages where you have your own analytics.
          - `carousel_item_index` integer, nullable: Only set when the button belongs to a card inside a carousel: 0-based index of the card in the carousel's `items`.
      - DelayFlowNode: Pauses the flow for a fixed duration before continuing. Useful to avoid spamming the user and to make the bot feel more human.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'DELAY'
        - `data` DelayNodeData, required: Action: pause the flow for `delay_duration` `delay_unit`s before continuing to the next node. Use to space out messages so the bot doesn't feel spammy.
          - `delay_duration` integer, required: How long to wait (positive integer).
          - `delay_unit` 'SECONDS' | 'MINUTES' | 'HOURS' | 'DAYS', required: Unit a user entered a wait duration in. Shared by the legacy automation step delays (`StepDelay.delay_unit`) and the flow builder's DELAY node / comment auto-reply delay. The backend always schedules with the canonical seconds value; this is what the UI round-trips so the editor can show the delay the way it was typed.
      - ConditionFlowNode: Branches the flow based on contact/event attributes. Has multiple outgoing edges keyed by `sourceHandle` (each matching a `source_handle` in the `conditions` list). Always include a FAILED catch-all branch.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'CONDITION'
        - `data` ConditionNodeData, required: Action: branch the flow based on contact/event attributes. Each entry in `conditions` defines one branch with its own `source_handle`; the flow takes the first matching branch.
          - `conditions` ConditionBase[], required: Ordered list of branch rules. Evaluated top-to-bottom; first match wins. Always include a FAILED catch-all as the last rule to handle 'none of the above'.
            - `type` 'IF_USER_FOLLOWED_ME' | 'IF_BUSINESS_FOLLOWS_USER' | 'VERIFIED_ON_INSTAGRAM' | 'FOLLOWERS_COUNT' | 'CONTACT_EMAIL' | 'CONTACT_PHONE' | 'CONTACT_FIELD', required: What a condition node evaluates against the contact/event. IF_USER_FOLLOWED_ME / IF_BUSINESS_FOLLOWS_USER / VERIFIED_ON_INSTAGRAM / FOLLOWERS_COUNT are Instagram-only (require Graph API fields not exposed on Facebook Pages); when the flow targets a Facebook Page they are short-circuited server-side. CONTACT_EMAIL / CONTACT_PHONE work on both surfaces. CONTACT_FIELD evaluates a custom User Field on the contact (set `field_key` to the field's slug); comparison semantics follow the field's declared type.
            - `source_handle` union, required: Outgoing handle id this rule routes to when matched. Use 'SUCCESS' / 'FAILED' for the standard two-way branch, or a custom handle id for multi-way branches (e.g. tiered FOLLOWERS_COUNT conditions).
              - …
            - `response_type` 'TEXT' | 'BOOLEAN' | 'NUMBER': Data type a condition resolves to. Determines which `check` operators and `value` shape are valid. BOOLEAN: true/false condition like 'is verified'. NUMBER: comparable like follower count. TEXT: string like contact email.
            - `check` 'EQUALS' | 'CONTAINS' | 'NOT_CONTAINS' | 'BEGINS_WITH' | 'ENDS_WITH' | 'CONTAINS_WHOLE_WORD' | 'CONTAINS_WHOLE_PHRASE' | 'GREATER_THAN' | 'LESS_THAN' | 'HAS_VALUE' | 'NOT_EQUALS' | 'NOT_HAS_VALUE': Keyword check type for user sends message trigger.
            - `value` union: Right-hand side of the comparison. Type must match `response_type` (bool, int, or str). Leave null for HAS_VALUE / NOT_HAS_VALUE.
              - …
            - `field_key` string, nullable: Slug of the custom User Field to evaluate. Required when type=CONTACT_FIELD, ignored otherwise.
      - CollectUserInfoFlowNode: Prompts the user for an email/phone and saves it on the Contact. Two outgoing handles: SUCCESS (collected & saved) and FAILED (max_retries exceeded).
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'COLLECT_USER_INFO'
        - `data` CollectUserInfoNodeData, required: Action: prompt the user for a piece of info in the DM, then save it. EMAIL/PHONE validate the reply and store it on the Contact record. ANY accepts any non-empty reply and stores it in the custom User Field named by `save_to_field_key`. Retries on invalid input up to `max_retries` before falling through the FAILED handle.
          - `type` 'EMAIL' | 'PHONE' | 'ANY', required: What contact field a COLLECT_USER_INFO node prompts the user to provide. EMAIL/PHONE validate the reply and store it on the contact's fixed column. ANY accepts any non-empty reply; set `save_to_field_key` so the reply is stored in that custom User Field.
          - `message` string, required: Prompt text shown when asking for the info.
          - `error_message` string, required: Reply when the user's response is invalid; sent on each retry.
          - `max_retries` integer: How many invalid attempts before the node falls through to FAILED.
          - `save_to_field_key` string, nullable: Slug of a custom User Field to save the collected reply into. ALWAYS set this for type=ANY (otherwise the reply is accepted but stored nowhere); optional for EMAIL/PHONE, which always store to the contact's fixed column as well. The value is coerced to the field's declared type.
      - ActionFlowNode: Runs one or more side-effects (HTTP request, tag changes, contact deletion) then continues the flow. Use this to integrate with CRMs, webhooks, and Salesmorph's own tagging system.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'ACTION'
        - `data` ActionNodeData, required: Action: run one or more side-effects (CRM push, tag changes, custom field updates, etc.) then continue to the next node. Use this for the 'do something then keep going' parts of a flow.
          - `actions` union[], required: Ordered list of side-effects to perform when this node fires. Include at least one action.
            - union
              - …
      - CollectPaymentFlowNode: Asks the contact to pay, through the creator's own Stripe/Razorpay account. Two outgoing handles: SUCCESS (payment captured) and FAILED (payment failed or the link went unpaid). Renders as "Connect payment gateway" until `data` is configured.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'COLLECT_PAYMENT'
        - `data` CollectPaymentNodeData, required: Action: charge the contact through the creator's OWN payment gateway. Two states, matching the two designs: UNCONFIGURED — every field below is null. The editor renders the "Connect payment gateway" placeholder. A node can sit like this while the flow is a draft; publishing a flow that still has one is what validation rejects. CONFIGURED — payment_provider_id, currency, amount, line_item_name and save_payment_link_to_field_key are all set. The editor renders the gateway name with "₹499.00 · <line_item_name>". Half-configured is not a state. The validator below refuses it, because a node with an amount but no gateway (or a gateway but no amount) would look configured in the editor and fail at run time, when a contact is already waiting on a payment link. Three outgoing handles: (default, no id) — "Then". Continues the conversation as soon as the payment link has been sent, while the contact is still deciding. Every other node has this handle; without it a payment node would stall the flow until a webhook arrived, which may be never. SUCCESS — the gateway reported the payment captured. FAILED — the payment failed, or the link went unpaid. SUCCESS and FAILED fire asynchronously, when the webhook lands, which is why they are separate from the immediate continuation.
          - `payment_provider_id` integer, nullable: Which connected gateway collects this — a UserPaymentProvider id from GET /v1/user_integrations/user_payments/provider/list. Null means the node is unconfigured.
          - `currency` string, nullable: ISO 4217, uppercase. Must be one the chosen gateway supports — see GET /v1/user_integrations/user_payments/currencies.
          - `amount` string, nullable: MAJOR units as a human types them: "499.00" means ₹499.00, not 49900. Converted to the gateway's minor units at link-creation time against the currency's ISO exponent. A STRING, not a Decimal. Flow nodes are persisted into flow_automation.nodes, a JSON column, and json.dumps cannot serialise a Decimal — saving any flow containing this node failed with "Object of type Decimal is not JSON serializable". A float would serialise but cannot hold 0.1 exactly, and this is money. The string is what the creator typed, parsed to Decimal at the one boundary that does arithmetic (services.to_minor_units).
          - `line_item_name` string, nullable: What the contact sees they are paying for.
          - `line_item_description` string, nullable: Optional longer description shown on the payment page.
          - `save_payment_link_to_field_key` string, nullable: Slug of a custom User Field the generated payment link is saved into, so later nodes can send it with {{user.<slug>}}. Required on a configured node — a link nothing can reference is a link the contact never receives. Defaults to `payment_link`. Same slug rules as CollectUserInfoNodeData.save_to_field_key.
      - RandomizerFlowNode: Splits traffic across its outgoing paths by percentage. Each path has its own `source_handle`; the matching edge must carry it as `sourceHandle` or that share of contacts dead-ends at this node.
        - `id` string, required: Unique node id within the flow. Use a UUID string (e.g. 'a7f3...'). Edges reference nodes by this id, so it must be stable once created.
        - `position` NodePosition, required: XYFlow canvas coordinate for a node. Used for visual layout only — has no effect on execution. Use `{x: 0, y: 0}` if you don't care about layout; the frontend will lay nodes out on first load.
          - `x` number, required: Horizontal pixel offset on the canvas.
          - `y` number, required: Vertical pixel offset on the canvas.
        - `parentId` string, nullable: Parent node id for grouped children. Required cases: (1) trigger nodes (USER_COMMENT_TRIGGER, USER_SENDS_MESSAGE, USER_SHARE_POST, USER_STORY_MENTION) must have the TRIGGER_GROUP's id as parentId. (2) MESSAGE_BUTTON nodes must have their parent message node's id as parentId (the BUTTON_TEXT_MESSAGE / BUTTON_TEXT_IMAGE_MESSAGE / CAROUSEL_BUTTON_TEXT_IMAGE_MESSAGE). All other nodes leave this null.
        - `extent` string, nullable: XYFlow visual constraint. Usually 'parent' when parentId is set. Leave null otherwise; the frontend infers this.
        - `width` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes — the editor populates this.
        - `height` number, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `measured` object, nullable: XYFlow runtime layout field. Do NOT set when generating new nodes.
        - `type` 'RANDOMIZER'
        - `data` RandomizerNodeData, required: Action: split traffic across 2-26 outgoing paths by percentage. Deliberately permissive on read: `paths` may be empty and the percentages need not total 100. Stored node data is re-validated every time a flow is loaded, so a strict rule here would take a live automation offline over a distribution the editor itself allows the user to save.
          - `paths` RandomizerPath[]: The outgoing paths, in editor order (first is 'A'). An empty list means the node is unconfigured and the flow stops there.
            - `source_handle` string, required: Outgoing handle id for this path. A uuid generated by the editor; the matching FlowEdge carries it as `sourceHandle`.
            - `percentage` integer: Share of traffic for this path. The editor keeps the paths summing to 100 but does not enforce it, so the executor treats these as relative weights and normalises by their actual total. A path at 0 never receives traffic.
          - `random_every_time` boolean: false (default): a contact is pinned to the path it first landed on, so re-entering the flow cannot move them between variants and A/B results stay readable. true: re-roll on every execution.
  - `edges` FlowEdge[]
    - `id` string, required: Unique edge id. Use a UUID string. Convention: 'e-<source>-<target>' or just a UUID.
    - `source` string, required: Id of the node this edge originates from. Must match an existing node `id`.
    - `target` string, required: Id of the node this edge points to. Must match an existing node `id`. Cannot equal `source` (no self-loops).
    - `sourceHandle` union: Which outgoing handle of `source` this edge uses. Required when `source` is a CONDITION node (use the `source_handle` from the matching ConditionBase rule, e.g. 'SUCCESS', 'FAILED', or a custom id), a RANDOMIZER node (use the `source_handle` of the path this edge belongs to), or a COLLECT_USER_INFO node ('SUCCESS' / 'FAILED'). For MESSAGE_BUTTON edges leave null — the button's id IS the handle. For all other source node types, leave null.
      - 'SUCCESS' | 'FAILED': Outgoing edge handle on a node with multiple branches. SUCCESS: condition matched / step completed normally. FAILED: condition did not match / step errored. CONDITION nodes always expose both; most other nodes expose only SUCCESS.
      - string
    - `targetHandle` string, nullable: Incoming handle on `target`. Almost always null; reserved for future use.
    - `type` string, nullable: Visual edge style (XYFlow). Leave null for the default; the frontend picks an appropriate style based on source/target types.

## Response `200`

Successful Response

- ApiModelResponseSchema
  - `id` integer, required

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/linktodm/apis/salesmorph.md) - [All operations](https://skmtc.dev/linktodm/apis/salesmorph/llms.txt) - [OpenAPI document](https://skmtc.dev/linktodm/apis/salesmorph/revisions/4af0ad5dbe92?raw)
