---
title: "Policy Scan Auto Fix Flow Automation"
method: POST
path: "/v1/ai/policy_scan_auto_fix_flow_automation"
tags: ["ai"]
---

# Policy Scan Auto Fix Flow Automation

`POST /v1/ai/policy_scan_auto_fix_flow_automation`

## 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/ai/policy_scan_auto_fix_flow_automation`
- Auth: HTTP bearer

Auto-fix policy violations on a draft FlowAutomationCreatePayload.

Same retry-until-clean loop as the automation variant. Rewrites text
inside flow nodes (message body, title/subtitle, button labels,
carousel items, trigger keywords, ai_prompts).

## Query parameters

- `exclude_flow_automation_id` integer, nullable: When fixing an EDIT, pass the current flow_automation id so the post-fix re-scan's keyword-reuse + content-reuse checks don't flag the flow's own data against itself.

## Request body

- PolicyFixFlowAutomationPayload: Body for POST /api/v1/ai/policy_scan_auto_fix_flow_automation. Carries the full draft FlowAutomationCreatePayload + the latest scan result. The node graph is validated against the discriminated FlowNode union before the fixer ever sees it, so the fixer can rely on shapes matching `app/api/v1/flow_automation/node_schemas.py`.
  - `payload` FlowAutomationCreatePayload, required: Payload for creating a new 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.
              - …
            - `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.
              - …
        - 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`.
              - …
        - 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.
              - …
            - `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'.
              - …
        - 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.
              - …
        - 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.
              - …
            - `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.
    - `facebook_account_id` string, nullable
    - `instagram_business_account_id` string, nullable
    - `instagram_user_id` string, nullable
    - `media_source` 'INSTAGRAM' | 'FACEBOOK'
  - `scan_result` PolicyScanResponse, required: Response body for the policy_scan_automation / policy_scan_flow_automation endpoints. Pre-creation scans, so no automation_id is present. Note: model name is intentionally NOT exposed in the response. It's captured in Axiom logs for ops/cost monitoring but kept private — callers shouldn't depend on it.
    - `automation_type` 'automation' | 'flow_automation', required
    - `scanned_at` string, required
    - `policy_version` string, required
    - `verdict` 'allow' | 'warn' | 'block', required
    - `verdict_message` string, required
    - `verdict_category` string, nullable
    - `totals` ScanTotals, required
      - `items_total` integer, required
      - `items_scanned` integer, required
      - `items_failed` integer, required
      - `items_with_violations` integer, required
    - `highest_severity` 'none' | 'low' | 'medium' | 'high', required
    - `items` ScanItemResult[], required
      - `item_id` string, required
      - `item_type` string, required
      - `content_preview` string, required
      - `status` 'scanned' | 'scan_failed', required
      - `highest_severity` 'none' | 'low' | 'medium' | 'high', required
      - `violations` Violation[]
        - `category` string, required: Key of the Meta policy category. Must match a key from app.api.v1.ai.policy_scan.meta_policies.
        - `severity` 'low' | 'medium' | 'high', required
        - `evidence` string, required: Verbatim quote from the content showing the violation.
        - `explanation` string, required: One-sentence mapping of the evidence to the named category.
        - `policy_url` string, nullable

## Response `200`

Successful Response

- PolicyFixFlowAutomationResponse: Response body for POST /api/v1/ai/policy_scan_auto_fix_flow_automation. Typed payload — frontend reads the rewritten node graph directly into the form's $formData without runtime type assertion.
  - `payload` FlowAutomationCreatePayload, required: Payload for creating a new 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.
              - …
            - `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.
              - …
        - 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`.
              - …
        - 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.
              - …
            - `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'.
              - …
        - 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.
              - …
        - 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.
              - …
            - `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.
    - `facebook_account_id` string, nullable
    - `instagram_business_account_id` string, nullable
    - `instagram_user_id` string, nullable
    - `media_source` 'INSTAGRAM' | 'FACEBOOK'
  - `changes` RawFix[], required
    - `item_id` string, required
    - `action` 'rewrite' | 'remove', required
    - `new_content` string, nullable
    - `rationale` string, nullable
  - `static_fixes_applied` string[]
  - `attempts` integer
  - `verified_clean` boolean

## 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)
