{
  "name": "IntakeSync: JetFormBuilder repeat fields to Airtable (flatten + child rows)",
  "flow": [
    {
      "id": 1,
      "module": "gateway:CustomWebHook",
      "version": 1,
      "parameters": {
        "maxResults": 1
      },
      "mapper": {},
      "metadata": {
        "designer": {
          "x": 0,
          "y": 0,
          "name": "JetFormBuilder intake webhook"
        },
        "restore": {
          "parameters": {
            "hook": {
              "data": {
                "editable": "true"
              },
              "label": "JetFormBuilder intake"
            }
          }
        },
        "parameters": [
          {
            "name": "hook",
            "type": "hook:gateway-webhook",
            "label": "Webhook",
            "required": true
          },
          {
            "name": "maxResults",
            "type": "number",
            "label": "Maximum number of results"
          }
        ],
        "notes": "JetFormBuilder posts the submission form encoded, and a repeat field arrives as dealbreakers[0][criterion], dealbreakers[1][criterion] and so on. Make expands that into an array of collections, so the rows are addressable as {{1.dealbreakers}}. Run Determine Data Structure once from a submission that has at least one repeat row filled, otherwise the field is missing from the structure."
      }
    },
    {
      "id": 2,
      "module": "util:SetVariables",
      "version": 1,
      "parameters": {
        "scope": "roundtrip"
      },
      "filter": {
        "name": "Valid intake (a well-formed email is present)",
        "conditions": [
          [
            {
              "a": "{{1.email}}",
              "o": "exist"
            },
            {
              "a": "{{trim(1.email)}}",
              "b": "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$",
              "o": "text:pattern"
            }
          ]
        ]
      },
      "mapper": {
        "scope": "roundtrip",
        "variables": [
          {
            "name": "email_norm",
            "value": "{{lower(trim(1.email))}}"
          },
          {
            "name": "rows_count",
            "value": "{{length(1.dealbreakers)}}"
          },
          {
            "name": "rows_summary",
            "value": "{{if(length(1.dealbreakers) > 0; join(map(1.dealbreakers; \"criterion\"); \"; \"); \"\")}}"
          }
        ]
      },
      "metadata": {
        "designer": {
          "x": 300,
          "y": 0,
          "name": "Normalize, count rows, flatten"
        },
        "notes": "email_norm is the dedupe key. rows_count is safe on an empty repeat field: length() returns 0 for the empty string JetFormBuilder sends when the applicant adds no rows. rows_summary is guarded because map() raises on a non-array; if() short-circuits, so the join never runs on an empty repeat field."
      }
    },
    {
      "id": 3,
      "module": "airtable:ActionSearchRecords",
      "version": 3,
      "parameters": {},
      "mapper": {
        "base": "appMATCHMKR000000",
        "table": "tblClients00000000",
        "formula": "AND({Email}!=\"\", LOWER({Email})=LOWER(\"{{2.email_norm}}\"))",
        "maxRecords": "1",
        "useColumnId": false
      },
      "metadata": {
        "designer": {
          "x": 600,
          "y": 0,
          "name": "Airtable: search client by email"
        },
        "notes": "Same dedupe as the first scenario. {{3.__IMTLENGTH__}} is 0 for a new client and 1 for a returning one."
      }
    },
    {
      "id": 4,
      "module": "builtin:BasicRouter",
      "version": 1,
      "parameters": {},
      "mapper": null,
      "metadata": {
        "designer": {
          "x": 900,
          "y": 0,
          "name": "Client row, then the repeat rows"
        },
        "notes": "Routes run in order: write the client, clear that client's old repeat rows, then write the current ones. Clearing before writing is what keeps a resubmission from doubling the rows."
      },
      "routes": [
        {
          "flow": [
            {
              "id": 5,
              "module": "airtable:ActionCreateRecord",
              "version": 3,
              "parameters": {},
              "filter": {
                "name": "New client (search found nothing)",
                "conditions": [
                  [
                    {
                      "a": "{{3.__IMTLENGTH__}}",
                      "b": "0",
                      "o": "number:equal"
                    }
                  ]
                ]
              },
              "mapper": {
                "base": "appMATCHMKR000000",
                "table": "tblClients00000000",
                "record": {
                  "Name": "{{1.full_name}}",
                  "Email": "{{2.email_norm}}",
                  "Phone": "{{1.phone}}",
                  "City": "{{1.city}}",
                  "Seeking": "{{1.seeking}}",
                  "Source": "{{1.how_heard}}",
                  "Status": "New",
                  "First Seen": "{{now}}",
                  "Last Updated": "{{now}}",
                  "Intake Count": "1",
                  "Preferences Summary": "{{2.rows_summary}}",
                  "Preference Rows": "{{2.rows_count}}"
                },
                "typecast": true,
                "useColumnId": false
              },
              "onerror": [
                {
                  "id": 100,
                  "module": "builtin:Break",
                  "version": 1,
                  "mapper": {
                    "count": "3",
                    "interval": "5"
                  },
                  "metadata": {
                    "designer": {
                      "x": 0,
                      "y": 0,
                      "name": "Retry, then park as incomplete"
                    },
                    "notes": "On an Airtable error while creating the client record, retry 3 times at 5 minute intervals, then store the run as an incomplete execution to resume. No submission is dropped."
                  }
                }
              ],
              "metadata": {
                "designer": {
                  "x": 1200,
                  "y": -300,
                  "name": "Airtable: create client"
                },
                "notes": "The flattened summary and the row count live on the client record too, so the repeat field is readable without opening the linked table."
              }
            }
          ]
        },
        {
          "flow": [
            {
              "id": 6,
              "module": "airtable:ActionUpdateRecords",
              "version": 3,
              "parameters": {},
              "filter": {
                "name": "Returning client (search found a match)",
                "conditions": [
                  [
                    {
                      "a": "{{3.__IMTLENGTH__}}",
                      "b": "0",
                      "o": "number:greater"
                    }
                  ]
                ]
              },
              "mapper": {
                "base": "appMATCHMKR000000",
                "table": "tblClients00000000",
                "id": "{{3.id}}",
                "record": {
                  "Name": "{{1.full_name}}",
                  "Phone": "{{1.phone}}",
                  "City": "{{1.city}}",
                  "Seeking": "{{1.seeking}}",
                  "Source": "{{1.how_heard}}",
                  "Last Updated": "{{now}}",
                  "Intake Count": "{{add(3.Intake Count; 1)}}",
                  "Preferences Summary": "{{2.rows_summary}}",
                  "Preference Rows": "{{2.rows_count}}"
                },
                "typecast": true,
                "useColumnId": false
              },
              "onerror": [
                {
                  "id": 101,
                  "module": "builtin:Break",
                  "version": 1,
                  "mapper": {
                    "count": "3",
                    "interval": "5"
                  },
                  "metadata": {
                    "designer": {
                      "x": 0,
                      "y": 0,
                      "name": "Retry, then park as incomplete"
                    },
                    "notes": "On an Airtable error while updating the client record, retry 3 times at 5 minute intervals, then store the run as an incomplete execution to resume. No submission is dropped."
                  }
                }
              ],
              "metadata": {
                "designer": {
                  "x": 1200,
                  "y": -100,
                  "name": "Airtable: update client"
                },
                "notes": "First Seen and the email key are left alone. The summary and the row count are refreshed from this submission."
              }
            }
          ]
        },
        {
          "flow": [
            {
              "id": 7,
              "module": "airtable:ActionSearchRecords",
              "version": 3,
              "parameters": {},
              "mapper": {
                "base": "appMATCHMKR000000",
                "table": "tblPrefs00000000000",
                "formula": "LOWER({Client Email})=LOWER(\"{{2.email_norm}}\")",
                "maxRecords": "100",
                "useColumnId": false
              },
              "metadata": {
                "designer": {
                  "x": 1200,
                  "y": 100,
                  "name": "Airtable: find old repeat rows"
                },
                "notes": "Returns one bundle per existing row for this client, so the delete below runs once per row. If nothing is found this route simply stops, which is why clearing sits on its own route and not in front of the write."
              }
            },
            {
              "id": 8,
              "module": "airtable:ActionDeleteRecord",
              "version": 3,
              "parameters": {},
              "mapper": {
                "base": "appMATCHMKR000000",
                "table": "tblPrefs00000000000",
                "id": "{{7.id}}",
                "useColumnId": false
              },
              "metadata": {
                "designer": {
                  "x": 1500,
                  "y": 100,
                  "name": "Airtable: delete old repeat row"
                },
                "notes": "Clear then rewrite. Without this, someone resubmitting with two dealbreakers ends up with four rows and no way to tell which pair is current."
              }
            }
          ]
        },
        {
          "flow": [
            {
              "id": 9,
              "module": "builtin:BasicFeeder",
              "version": 1,
              "parameters": {},
              "filter": {
                "name": "Repeat rows present",
                "conditions": [
                  [
                    {
                      "a": "{{2.rows_count}}",
                      "b": "0",
                      "o": "number:greater"
                    }
                  ]
                ]
              },
              "mapper": {
                "array": "{{1.dealbreakers}}"
              },
              "metadata": {
                "designer": {
                  "x": 1200,
                  "y": 300,
                  "name": "Iterate the repeat rows"
                },
                "notes": "The filter is load bearing. An applicant who adds no rows sends an empty string, not an empty array, and an Iterator handed an empty string fails the whole run with a 500, which loses the submission. Gating on rows_count means the Iterator only ever sees a real array."
              }
            },
            {
              "id": 10,
              "module": "builtin:BasicRouter",
              "version": 1,
              "parameters": {},
              "mapper": null,
              "metadata": {
                "designer": {
                  "x": 1500,
                  "y": 300,
                  "name": "Complete row, or needs review"
                }
              },
              "routes": [
                {
                  "flow": [
                    {
                      "id": 11,
                      "module": "airtable:ActionCreateRecord",
                      "version": 3,
                      "parameters": {},
                      "filter": {
                        "name": "Row has a criterion",
                        "conditions": [
                          [
                            {
                              "a": "{{length(9.criterion)}}",
                              "b": "0",
                              "o": "number:greater"
                            }
                          ]
                        ]
                      },
                      "mapper": {
                        "base": "appMATCHMKR000000",
                        "table": "tblPrefs00000000000",
                        "record": {
                          "Client Email": "{{2.email_norm}}",
                          "Criterion": "{{9.criterion}}",
                          "Importance": "{{9.importance}}",
                          "Row": "{{9.__IMTINDEX__}}",
                          "Submitted At": "{{now}}"
                        },
                        "typecast": true,
                        "useColumnId": false
                      },
                      "onerror": [
                        {
                          "id": 102,
                          "module": "builtin:Break",
                          "version": 1,
                          "mapper": {
                            "count": "3",
                            "interval": "5"
                          },
                          "metadata": {
                            "designer": {
                              "x": 0,
                              "y": 0,
                              "name": "Retry, then park as incomplete"
                            },
                            "notes": "On an Airtable error while writing a repeat row, retry 3 times at 5 minute intervals, then store the run as an incomplete execution to resume. No submission is dropped."
                          }
                        }
                      ],
                      "metadata": {
                        "designer": {
                          "x": 1800,
                          "y": 200,
                          "name": "Airtable: create repeat row"
                        },
                        "notes": "One Airtable row per repeat row, keyed back to the client by email. Row keeps the order the applicant entered them in."
                      }
                    }
                  ]
                },
                {
                  "flow": [
                    {
                      "id": 12,
                      "module": "airtable:ActionCreateRecord",
                      "version": 3,
                      "parameters": {},
                      "filter": {
                        "name": "Row is incomplete",
                        "conditions": [
                          [
                            {
                              "a": "{{length(9.criterion)}}",
                              "b": "0",
                              "o": "number:equal"
                            }
                          ]
                        ]
                      },
                      "mapper": {
                        "base": "appMATCHMKR000000",
                        "table": "tblReview0000000000",
                        "record": {
                          "Client Email": "{{2.email_norm}}",
                          "Row": "{{9.__IMTINDEX__}}",
                          "What Arrived": "importance={{9.importance}}",
                          "Reason": "Repeat row has no criterion",
                          "Seen At": "{{now}}"
                        },
                        "typecast": true,
                        "useColumnId": false
                      },
                      "metadata": {
                        "designer": {
                          "x": 1800,
                          "y": 400,
                          "name": "Airtable: flag for review"
                        },
                        "notes": "A half filled repeat row is the one case worth being loud about. Writing it as a blank Criterion would look like a real preference; dropping it silently hides that the form let someone submit it. It lands here instead, with the row number, and the rest of the submission still goes through."
                      }
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "metadata": {
    "instant": true,
    "version": 1,
    "scenario": {
      "roundtrips": 1,
      "maxErrors": 3,
      "autoCommit": true,
      "autoCommitTriggerLast": true,
      "sequential": false,
      "slots": null,
      "confidential": false,
      "dataloss": false,
      "dlq": false,
      "freshVariables": false
    },
    "designer": {
      "orphans": []
    },
    "zone": "eu1.make.com"
  }
}
