This workflow follows the Airtable → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "01-intake-storage",
"nodes": [
{
"parameters": {
"respondWith": "json",
"responseBody": "{\n \"error\": \"Unauthorized\",\n \"message\": \"Invalid or missing authentication token.\"\n}",
"options": {
"responseCode": 401
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
224,
-80
],
"id": "257518d4-2bbc-4750-bba9-3064dd0984fb",
"name": "ACK : Unauthorized",
"notesInFlow": true,
"notes": "401 response for invalid/missing webhook token"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "{\n \"status\": \"received\",\n \"message\": \"Application received successfully.\"\n}",
"options": {
"responseCode": 200
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
224,
-272
],
"id": "37ea3389-1ab4-44cd-93f1-140638143984",
"name": "ACK : Submission Received",
"notesInFlow": true,
"notes": "200 OK acknowledgment to Tally. Confirms submission received."
},
{
"parameters": {
"httpMethod": "POST",
"path": "your-agency-intake",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
0,
-176
],
"id": "77823c2e-8ab3-4e72-8ce9-9cd5a9175a5d",
"name": "Form Intake Trigger",
"notesInFlow": true,
"onError": "continueErrorOutput",
"notes": "Tally form webhook : Noir Collective Prototype \nIntake pipeline entry point"
},
{
"parameters": {
"operation": "search",
"base": {
"__rl": true,
"value": "appYcE1r1ZddkVbK3",
"mode": "list",
"cachedResultName": "SPeXecute TS CRM",
"cachedResultUrl": "https://airtable.com/appYcE1r1ZddkVbK3"
},
"table": {
"__rl": true,
"value": "tblwHQEfJ1QqKrNFi",
"mode": "list",
"cachedResultName": "CLIENT CONFIG TP",
"cachedResultUrl": "https://airtable.com/appYcE1r1ZddkVbK3/tblwHQEfJ1QqKrNFi"
},
"filterByFormula": "{Agency Slug} = 'noir-collective-france'",
"returnAll": false,
"limit": 1,
"options": {}
},
"type": "n8n-nodes-base.airtable",
"typeVersion": 2.2,
"position": [
448,
-272
],
"id": "51cd7aaf-6978-46e0-8f75-8310cc6fb5ae",
"name": "Read Agency Slug",
"notesInFlow": true,
"retryOnFail": true,
"credentials": {
"airtableTokenApi": {
"name": "<your credential>"
}
},
"notes": "Read Noir CLIENT CONFIG from Airtable. Single row returned. All pipeline nodes reference this"
},
{
"parameters": {
"jsCode": "// ============================================================\n// NODE 3 \u2014 Parse & Validate Form Data\n// Built against real Tally payload \u2014 noir-collective-france\n// Last updated: April 2026\n// ============================================================\n\nconst FIELD_MAP = {\n 'question_AJBrbB': 'full_name',\n 'question_B1xEb7': 'email',\n 'question_kANbR6': 'phone',\n 'question_PEYr95': 'gender',\n 'question_7DvbJ9': 'is_minor',\n 'question_vBPXxA': 'date_of_birth',\n 'question_KoVpBM': 'location_city',\n 'question_LGPDMG': 'zip_code',\n 'question_pADePV': 'location_country',\n 'question_1K9W2M': 'nationality',\n 'question_MOE1dY': 'right_to_work_countries',\n 'question_JRO1kJ': 'right_to_work_other',\n 'question_gA9bNN': 'instagram_url',\n 'question_yyJXq0': 'languages_spoken',\n 'question_XG45BO': 'languages_other',\n 'question_8kZNpA': 'height_cm',\n 'question_0MeVjZ': 'bust_cm',\n 'question_zz7EQ1': 'waist_cm',\n 'question_E1D6lX': 'hips_cm',\n 'question_5kZXqM': 'dress_size',\n 'question_dA9b2z': 'shoe_size_eu',\n 'question_YO4jzq': 'hair_color',\n 'question_D17qvb': 'eye_color',\n 'question_lA6al5': 'hair_texture',\n 'question_R4DWBl': 'skin_tone',\n 'question_oB29V1': 'modeling_experience',\n 'question_G1ReBj': 'experience_details',\n 'question_OP7QBK': 'currently_represented',\n 'question_Vlzp1a': 'current_agencies',\n 'question_PEzRBQ': 'was_scouted',\n 'question_E1xqbo': 'scouted_by',\n 'question_rKoDrM': 'special_skills',\n 'question_4kKaNb': 'availability',\n 'question_bkr922': 'guardian_name',\n 'question_AJDK6W': 'guardian_email',\n 'question_B1DKLA': 'guardian_phone',\n 'question_kAW90J_d18a3420-2d4d-44fd-8cdf-38d8bc9d0d33': 'guardian_consent',\n 'question_jMle9R': 'photo_headshot',\n 'question_2kKjxp': 'photo_full_body',\n 'question_xQJVNG': 'photo_profile',\n 'question_R4DWBp': 'photo_three_quarter',\n 'question_rKLYOl': 'photo_polaroid',\n 'question_vBLr0d_0bb10bdc-f15d-41dd-b574-4acf63bd2d5d': 'gdpr_consent',\n 'question_oBdxYO_db5144bf-9125-4f71-8923-536b4d6b9e6f': 'ai_consent',\n 'question_KoDePk_ce4f8f70-ab47-4ca5-9af4-10823e74710a': 'accuracy_declaration',\n};\n\nfunction resolveOptions(field) {\n if (!field.options || !field.value) return field.value;\n const optionMap = {};\n field.options.forEach(o => { optionMap[o.id] = o.text; });\n if (Array.isArray(field.value)) {\n return field.value.map(id => optionMap[id] || id).filter(Boolean);\n }\n return optionMap[field.value] || field.value;\n}\n\nconst fields = $('Form Intake Trigger').first().json.body.data.fields;\nconst raw = {};\n\nfields.forEach(field => {\n const fieldName = FIELD_MAP[field.key];\n if (!fieldName) return;\n if (field.type === 'FILE_UPLOAD') {\n raw[fieldName] = Array.isArray(field.value) ? field.value : [];\n } else if (field.type === 'DROPDOWN') {\n raw[fieldName] = resolveOptions(field);\n if (Array.isArray(raw[fieldName])) raw[fieldName] = raw[fieldName][0] || null;\n } else if (field.type === 'CHECKBOXES' || field.type === 'MULTI_SELECT') {\n raw[fieldName] = resolveOptions(field);\n } else if (field.type === 'MULTIPLE_CHOICE') {\n raw[fieldName] = resolveOptions(field);\n if (Array.isArray(raw[fieldName])) raw[fieldName] = raw[fieldName][0] || null;\n } else {\n raw[fieldName] = field.value;\n }\n});\n\nconst config = $('Read Agency Slug').first().json.fields;\n\nconst clean = (val) => (val || '').toString().trim();\nconst cleanLower = (val) => clean(val).toLowerCase();\nconst toFloat = (val) => { const n = parseFloat(val); return isNaN(n) ? null : n; };\nconst toBool = (val) => {\n if (val === true || val === 'true' || val === 1) return true;\n if (val === false || val === 'false' || val === 0) return false;\n return null;\n};\nconst toArray = (val) => {\n if (!val) return [];\n if (Array.isArray(val)) return val;\n if (typeof val === 'string') return val.split(',').map(s => s.trim()).filter(Boolean);\n return [];\n};\nconst toDate = (val) => {\n if (!val) return null;\n const d = new Date(val);\n return isNaN(d.getTime()) ? null : d.toISOString().split('T')[0];\n};\n\nconst data = {\n full_name: clean(raw.full_name),\n email: cleanLower(raw.email),\n phone: clean(raw.phone),\n date_of_birth: toDate(raw.date_of_birth),\n location_city: clean(raw.location_city),\n location_country: clean(raw.location_country),\n nationality: clean(raw.nationality),\n instagram_url: clean(raw.instagram_url),\n language: 'EN',\n gender: clean(raw.gender),\n height_cm: toFloat(raw.height_cm),\n bust_cm: toFloat(raw.bust_cm),\n waist_cm: toFloat(raw.waist_cm),\n hips_cm: toFloat(raw.hips_cm),\n shoe_size_eu: toFloat(raw.shoe_size_eu),\n hair_color: clean(raw.hair_color),\n eye_color: clean(raw.eye_color),\n skin_tone: clean(raw.skin_tone),\n hair_texture: clean(raw.hair_texture),\n modeling_experience: raw.modeling_experience === 'Yes' ? 'Yes' : 'No',\n experience_details: clean(raw.experience_details),\n languages_spoken: toArray(raw.languages_spoken),\n special_skills: clean(raw.special_skills),\n right_to_work: Array.isArray(raw.right_to_work_countries) && raw.right_to_work_countries.length > 0,\n availability: Array.isArray(raw.availability) ? raw.availability.join(', ') : clean(raw.availability),\n gdpr_consent: toBool(raw.gdpr_consent),\n ai_consent: toBool(raw.ai_consent),\n guardian_consent: toBool(raw.guardian_consent) || null,\n consent_date: new Date().toISOString(),\n agency_slug: config['Agency Slug'] || 'noir-collective-france',\n application_source: 'tally_form',\n platform_type: 'tally',\n application_stage: 'processing',\n status: 'active',\n batch_processing_status: 'queued',\n storage_type: 'r2',\n is_reapplication: false,\n cooldown_overridden: false,\n analysis_run_count: 0,\n photo_resubmission_count: 0,\n _photos: {\n headshot: raw.photo_headshot || [],\n full_body: raw.photo_full_body || [],\n profile: raw.photo_profile || [],\n three_quarter: raw.photo_three_quarter || [],\n polaroid: raw.photo_polaroid || [],\n },\n};\n\nif (data.languages_spoken.includes('French')) data.language = 'FR';\nelse if (data.languages_spoken.includes('Spanish')) data.language = 'ES';\nelse if (data.languages_spoken.includes('German')) data.language = 'DE';\nelse if (data.languages_spoken.includes('Arabic')) data.language = 'AR';\nelse if (data.languages_spoken.includes('Hindi')) data.language = 'HI';\nelse data.language = 'EN';\n\nconst errors = [];\nif (!data.full_name) errors.push('full_name is required');\nif (!data.email) errors.push('email is required');\nif (!data.email.includes('@')) errors.push('email format invalid');\nif (!data.gender) errors.push('gender is required');\nif (!data.date_of_birth) errors.push('date_of_birth is required');\nif (!data.location_city) errors.push('location_city is required');\nif (!data.location_country) errors.push('location_country is required');\nif (data.gdpr_consent !== true) errors.push('gdpr_consent must be true \u2014 hard stop');\nif (data.ai_consent !== true) errors.push('ai_consent must be true \u2014 hard stop');\nif (!data._photos.headshot.length) errors.push('headshot photo is required');\n\nif (errors.length > 0) {\n throw new Error('Validation failed: ' + errors.join(' | '));\n}\n\nreturn [{ json: data }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
672,
-272
],
"id": "500a2164-2f4d-49c2-8b77-61948b090502",
"name": "Code: Parse & Validate Form Data",
"notesInFlow": true,
"notes": "Parse + validate Tally payload. \nMaps to exact Supabase schema.\nHard stops on missing consent, missing required fields, or invalid data types."
},
{
"parameters": {
"operation": "getAll",
"tableId": "intake",
"limit": 1,
"filters": {
"conditions": [
{
"keyName": "email",
"condition": "eq",
"keyValue": "={{ $('Code: Parse & Validate Form Data').first().json.email }}"
},
{
"keyName": "phone",
"condition": "eq",
"keyValue": "={{ $('Code: Parse & Validate Form Data').first().json.phone }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
896,
-272
],
"id": "6ee416d4-a071-43c5-9b41-873bbff9e040",
"name": "Duplicate Application Check",
"retryOnFail": true,
"notesInFlow": true,
"alwaysOutputData": true,
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "Check if email already exists in intake table. \nReturns 0 rows if new applicant. \nReturns 1+ rows if duplicate or reapplication."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "93b2eb41-f2e9-435d-b16c-752f79879905",
"leftValue": "={{ $('Duplicate Application Check').first().json.intake_id }}",
"rightValue": 0,
"operator": {
"type": "string",
"operation": "empty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1120,
-272
],
"id": "ecc9646a-ff3d-4534-bd98-340f61565022",
"name": "IF: New or Returning Applicant",
"notesInFlow": true,
"notes": "TRUE = new applicant, no duplicate found.\nFALSE = email or phone exists in system, \nroute to cooldown check."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "22647bf5-c404-44d9-a606-7191ef1155e1",
"leftValue": "={{ $('Code: Parse & Validate Form Data').first().json.gdpr_consent }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
},
{
"id": "0b8555b0-72ef-457c-9a13-466275a9eec9",
"leftValue": "={{ $('Code: Parse & Validate Form Data').first().json.ai_consent }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1792,
-272
],
"id": "04350146-4a33-4df3-9668-baa3ad166296",
"name": "IF: Consent Gate",
"retryOnFail": true,
"notes": "TRUE = both GDPR and AI consent given. Continue.\nFALSE = consent missing. Stop \u2014 cannot process."
},
{
"parameters": {
"errorMessage": "Consent not given \u2014 pipeline stopped."
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
2016,
-176
],
"id": "1a24936e-670e-42ef-957c-26c1affdc21d",
"name": "Stop: Consent Missing"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "b6f0e59b-d35f-4c30-ad80-182f020f40bf",
"leftValue": "={{ $('Code: Parse & Validate Form Data').first().json.date_of_birth }}",
"rightValue": "={{ DateTime.now().minus({years: 18}).toISO() }}",
"operator": {
"type": "dateTime",
"operation": "after"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2016,
-368
],
"id": "4634db91-9a21-4b04-b47e-d8d766180287",
"name": "IF: Minor Flag Check",
"notes": "TRUE = applicant is under 18. Check guardian consent before continuing.\nFALSE = adult applicant. Continue"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 3
},
"conditions": [
{
"id": "7894b7c0-31f5-4697-94ac-b512ced39db5",
"leftValue": "= {{ $('Code: Parse & Validate Form Data').first().json.guardian_consent }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
},
{
"id": "e35de953-02df-4426-b6bd-9cc8821b296e",
"leftValue": "={{ $('Code: Parse & Validate Form Data').first().json.guardian_name }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
},
{
"id": "6e93f9fe-18e3-4f7e-944f-ec6fdfc02321",
"leftValue": "={{ $('Code: Parse & Validate Form Data').first().json.guardian_email }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
},
{
"id": "ed12305d-c9f8-4c9e-80a6-6145a2f2aab2",
"leftValue": "={{ $('Code: Parse & Validate Form Data').first().json.guardian_phone }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"looseTypeValidation": true,
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2240,
-448
],
"id": "29f1ba51-954a-42a9-8807-581d0f4b09ee",
"name": "IF: Guardian Consent Check"
},
{
"parameters": {
"errorMessage": "\"Minor applicant: guardian fields incomplete or consent not given. Cannot process application"
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
2464,
-496
],
"id": "fb805819-1b46-4be2-bcd2-90b808a4d97c",
"name": "Stop: Minor \u2014 Guardian Check Failed",
"notesInFlow": true,
"notes": "Fires when minor applicant has missing guardian fields or guardian consent not given"
},
{
"parameters": {
"jsCode": "const data = $('Code: Parse & Validate Form Data').first().json;\n\nconst batch_id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0;\n var v = c === 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n});\n\nconst images_in_batch = \n (data._photos.headshot.length > 0 ? 1 : 0) +\n (data._photos.full_body.length > 0 ? 1 : 0) +\n (data._photos.profile.length > 0 ? 1 : 0) +\n (data._photos.three_quarter.length > 0 ? 1 : 0) +\n (data._photos.polaroid.length > 0 ? 1 : 0);\n\nconst reapplication_eligible_date = new Date(\n Date.now() + 180 * 24 * 60 * 60 * 1000\n).toISOString().split('T')[0];\n\nreturn [{\n json: {\n ...data,\n batch_id: batch_id,\n images_in_batch: images_in_batch,\n reapplication_eligible_date: reapplication_eligible_date,\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2464,
-272
],
"id": "e287e8af-8225-48f0-865f-f1b805eb04a0",
"name": "Code: Generate Batch ID",
"notes": "Generates UUID batch_id that ties all media_forensics rows to this intake record.\nAlso counts actual photos submitted."
},
{
"parameters": {
"tableId": "intake",
"dataToSend": "autoMapInputData",
"inputsToIgnore": "_photos"
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
2688,
-272
],
"id": "bee2fdf9-a404-4f0e-a8f4-17dbda0c7249",
"name": "Write Initial Supabase Row",
"retryOnFail": true,
"notesInFlow": true,
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "First write to Supabase intake table.\nAutomaps all parsed form fields.\nExcludes _photos (handled separately in R2 upload node)"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $('Duplicate Application Check').first().json.reapplication_eligible_date }}",
"rightValue": "={{ DateTime.now().toISO() }}",
"operator": {
"type": "dateTime",
"operation": "after"
},
"id": "fa45e444-3f2b-4022-986f-658fb86bc9c9"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Cooldown Active \u2014 Reject"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 3
},
"conditions": [
{
"id": "1bb674a8-cc91-49fe-bec0-de3a9906c733",
"leftValue": "",
"rightValue": "",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Reapplication Allowed"
}
]
},
"looseTypeValidation": true,
"options": {}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
1344,
-208
],
"id": "489eb53d-20a4-405d-8761-e8a0f89a07d4",
"name": "Switch: Cooldown Check",
"notesInFlow": true,
"notes": "Cooldown Active -reject.\nreapplication_eligible_date is in the future \nReapplication Allowed: cooldown expired - set flag and continue."
},
{
"parameters": {
"errorMessage": "\"Cooldown active - reapplication not yet eligible.\""
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
1568,
-304
],
"id": "e74ab404-832c-494d-ab52-07788d64818d",
"name": "Stop: Cooldown Active"
},
{
"parameters": {
"jsCode": "const data = $('Code: Parse & Validate Form Data').first().json;\nconst existing = $('Duplicate Application Check').first().json;\n\nconst previous_count = existing.reapplication_count || 0;\n\nreturn [{\n json: {\n ...data,\n is_reapplication: true,\n reapplication_count: previous_count + 1,\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1568,
-112
],
"id": "7d78b7ff-5bcf-4037-886a-b15faf15d481",
"name": "Code: Set Reapplication Flag"
},
{
"parameters": {
"jsCode": "const supabase_row = $input.first().json;\nconst form_data = $('Code: Generate Batch ID').first().json;\n\nconst intake_id = supabase_row.intake_id;\nconst batch_id = form_data.batch_id;\nconst agency_slug = form_data.agency_slug;\n\n// Read thumbnail type from CLIENT CONFIG\nconst config = $('Read Agency Slug').first().json.fields;\nconst thumbnail_photo_type = (config['Thumbnail Photo Type'] || 'headshot').toLowerCase().replace(' ', '_');\n\n// Build month prefix YYYY-MM\nconst now = new Date();\nconst month_prefix = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');\n\nconst photos = [];\nconst photoTypes = ['headshot', 'full_body', 'profile', 'three_quarter', 'polaroid'];\n\nphotoTypes.forEach(type => {\n const photoArray = form_data._photos[type];\n if (photoArray && photoArray.length > 0) {\n photoArray.forEach(photo => {\n const ext = photo.mimeType === 'image/png' ? 'png'\n : photo.mimeType === 'image/jpeg' ? 'jpg'\n : photo.mimeType === 'image/webp' ? 'webp'\n : 'jpg';\n\n const r2_object_key = `${agency_slug}/intake/talent_prism/${month_prefix}/${intake_id}/${type}_${photo.id}.${ext}`;\n\n photos.push({\n intake_id: intake_id,\n batch_id: batch_id,\n agency_slug: agency_slug,\n photo_type: type,\n tally_url: photo.url,\n tally_id: photo.id,\n file_name: photo.name,\n mime_type: photo.mimeType,\n file_size_bytes: photo.size,\n image_file_size_kb: Math.round(photo.size / 1024),\n r2_object_key: r2_object_key,\n is_thumbnail: type === thumbnail_photo_type,\n processing_status: 'pending',\n photo_compliance_checked: false,\n tally_submission_time: form_data.consent_date,\n });\n });\n }\n});\n\nreturn photos.map(p => ({ json: p }));"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2912,
-272
],
"id": "49983da2-630a-42e2-9b1d-0513f360ba0a",
"name": "Code: Flatten Photos for Upload",
"notesInFlow": true,
"notes": "Flattens _photos object into individual items \u2014 one per photo.\nReads intake_id from Supabase output.\nBuilds R2 object key per photo.\nMarks thumbnail from CLIENT CONFIG."
},
{
"parameters": {
"url": "={{ $json.tally_url }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
3136,
-272
],
"id": "2f24e574-f7e1-416c-ad91-1079626d3308",
"name": "HTTP: Download Photo from Tally",
"retryOnFail": true,
"notesInFlow": true,
"onError": "continueErrorOutput",
"notes": "Downloads photo binary from temporary Tally URL. \n Must complete before URL expires (~1hr after submission)."
},
{
"parameters": {
"operation": "upload",
"bucketName": "spexecute-ts-prism-proto",
"fileName": "={{ $json.r2_object_key }}",
"additionalFields": {
"storageClass": "standard"
}
},
"type": "n8n-nodes-base.s3",
"typeVersion": 1,
"position": [
3360,
-368
],
"id": "c0622625-56e7-4b78-ae42-16a2fa588452",
"name": "S3: Upload Photo to R2",
"retryOnFail": true,
"notesInFlow": true,
"credentials": {
"s3": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput",
"notes": "Uploads photo binary to R2 bucket.\nFile path = r2_object_key.\nContent-Type set from mime_type automatically"
},
{
"parameters": {
"errorMessage": "\"Photo download failed \u2014 Tally URL may have expired."
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
3360,
-176
],
"id": "1d1b57c8-842b-4afe-86ce-01af211234fd",
"name": "HTTP Error Branch"
},
{
"parameters": {
"errorMessage": "Photo upload to R2 failed."
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
3584,
-272
],
"id": "b06436d2-d23f-4d8a-989e-8598d7c0cce4",
"name": "S3 Error Branch"
},
{
"parameters": {
"tableId": "media_forensics",
"fieldsUi": {
"fieldValues": [
{
"fieldId": "intake_id",
"fieldValue": "={{ $('Code: Flatten Photos for Upload').item.json.intake_id }}"
},
{
"fieldId": "batch_id",
"fieldValue": "={{ $('Code: Flatten Photos for Upload').item.json.batch_id }}"
},
{
"fieldId": "r2_object_key",
"fieldValue": "={{ $('Code: Flatten Photos for Upload').item.json.r2_object_key }}"
},
{
"fieldId": "storage_url",
"fieldValue": "={{ 'https://spexecute-ts-prism-proto.' + '8fb911303928533629b277039515d430' + '.r2.cloudflarestorage.com/' + $('Code: Flatten Photos for Upload').item.json.r2_object_key }}"
},
{
"fieldId": "is_thumbnail",
"fieldValue": "={{ $('Code: Flatten Photos for Upload').item.json.is_thumbnail }}"
},
{
"fieldId": "processing_status",
"fieldValue": "pending"
},
{
"fieldId": "photo_compliance_checked",
"fieldValue": "false"
},
{
"fieldId": "photo_type",
"fieldValue": "={{ $('Code: Flatten Photos for Upload').item.json.photo_type }}"
},
{
"fieldId": "image_file_size_kb",
"fieldValue": "={{ $('Code: Flatten Photos for Upload').item.json.image_file_size_kb }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
3584,
-464
],
"id": "1cd8fe12-8e74-4980-908c-479460732f2e",
"name": "Supabase: Write media_forensics Row",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"content": "# ZONE 1 \u2014 INTAKE & VALIDATION\nTally form intake, parsing, duplicate check,\nconsent gate, minor check, Supabase write,\nR2 photo upload, media_forensics write.",
"height": 832,
"width": 4848
},
"type": "n8n-nodes-base.stickyNote",
"position": [
-256,
-752
],
"typeVersion": 1,
"id": "98e91bea-afe5-46d8-b2c8-ec5f751251f3",
"name": "Sticky Note"
},
{
"parameters": {
"jsCode": "const data = $('Code: Generate Batch ID').first().json;\nconst config = $('Read Agency Slug').first().json.fields;\nconst gender = data.gender;\n\n// \u2500\u2500 READ THRESHOLDS FROM CONFIG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst f = {\n min_height: parseFloat(config['(F) Min Height']) || null,\n max_height: parseFloat(config['(F) Max Height']) || null,\n min_bust: parseFloat(config['(F) Min Bust']) || null,\n max_bust: parseFloat(config['(F) Max Bust']) || null,\n min_waist: parseFloat(config['(F) Min Waist']) || null,\n max_waist: parseFloat(config['(F) Max Waist']) || null,\n min_hips: parseFloat(config['(F) Min Hips']) || null,\n max_hips: parseFloat(config['(F) Max Hips']) || null,\n min_shoe: parseFloat(config['(F) Min Shoe']) || null,\n max_shoe: parseFloat(config['(F) Max Shoe']) || null,\n min_age: parseFloat(config['(F) Min Age']) || null,\n max_age: parseFloat(config['(F) Max Age']) || null,\n};\n\nconst m = {\n min_height: parseFloat(config['(M) Min Height']) || null,\n max_height: parseFloat(config['(M) Max Height']) || null,\n min_chest: parseFloat(config['(M) Min Chest']) || null,\n max_chest: parseFloat(config['(M) Max Chest']) || null,\n min_waist: parseFloat(config['(M) Min Waist']) || null,\n max_waist: parseFloat(config['(M) Max Waist']) || null,\n min_shoe: parseFloat(config['(M) Min Shoe']) || null,\n max_shoe: parseFloat(config['(M) Max Shoe']) || null,\n min_age: parseFloat(config['(M) Min Age']) || null,\n max_age: parseFloat(config['(M) Max Age']) || null,\n};\n\nconst nb = {\n min_height: parseFloat(config['(NB) Min Height']) || null,\n max_height: parseFloat(config['(NB) Max Height']) || null,\n min_waist: parseFloat(config['(NB) Min Waist']) || null,\n max_waist: parseFloat(config['(NB) Max Waist']) || null,\n min_shoe: parseFloat(config['(NB) Min Shoe']) || null,\n max_shoe: parseFloat(config['(NB) Max Shoe']) || null,\n min_age: parseFloat(config['(NB) Min Age']) || null,\n max_age: parseFloat(config['(NB) Max Age']) || null,\n};\n\n// \u2500\u2500 AGE CALCULATION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst dob = new Date(data.date_of_birth);\nconst today = new Date();\nlet age = today.getFullYear() - dob.getFullYear();\nconst mo = today.getMonth() - dob.getMonth();\nif (mo < 0 || (mo === 0 && today.getDate() < dob.getDate())) age--;\n\n// \u2500\u2500 HELPER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst checkRange = (val, min, max, label) => {\n const errors = [];\n if (min !== null && val !== null && val < min)\n errors.push(`${label} ${val} below minimum ${min}`);\n if (max !== null && val !== null && val > max)\n errors.push(`${label} ${val} above maximum ${max}`);\n return errors;\n};\n\nconst errors = [];\n\n// \u2500\u2500 FEMALE FILTERS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif (gender === 'Female') {\n errors.push(...checkRange(data.height_cm, f.min_height, f.max_height, 'Height(cm)'));\n errors.push(...checkRange(data.bust_cm, f.min_bust, f.max_bust, 'Bust(cm)'));\n errors.push(...checkRange(data.waist_cm, f.min_waist, f.max_waist, 'Waist(cm)'));\n errors.push(...checkRange(data.hips_cm, f.min_hips, f.max_hips, 'Hips(cm)'));\n errors.push(...checkRange(data.shoe_size_eu, f.min_shoe, f.max_shoe, 'Shoe(EU)'));\n errors.push(...checkRange(age, f.min_age, f.max_age, 'Age'));\n}\n\n// \u2500\u2500 MALE FILTERS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nelse if (gender === 'Male') {\n errors.push(...checkRange(data.height_cm, m.min_height, m.max_height, 'Height(cm)'));\n errors.push(...checkRange(data.bust_cm, m.min_chest, m.max_chest, 'Chest(cm)'));\n errors.push(...checkRange(data.waist_cm, m.min_waist, m.max_waist, 'Waist(cm)'));\n errors.push(...checkRange(data.shoe_size_eu, m.min_shoe, m.max_shoe, 'Shoe(EU)'));\n errors.push(...checkRange(age, m.min_age, m.max_age, 'Age'));\n}\n\n// \u2500\u2500 NON-BINARY FILTERS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nelse if (gender === 'Non Binary') {\n errors.push(...checkRange(data.height_cm, nb.min_height, nb.max_height, 'Height(cm)'));\n errors.push(...checkRange(data.waist_cm, nb.min_waist, nb.max_waist, 'Waist(cm)'));\n errors.push(...checkRange(data.shoe_size_eu, nb.min_shoe, nb.max_shoe, 'Shoe(EU)'));\n errors.push(...checkRange(age, nb.min_age, nb.max_age, 'Age'));\n}\n\n// \u2500\u2500 TRANS \u2192 NON-BINARY PATH \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nelse if (gender === 'Trans') {\n errors.push(...checkRange(data.height_cm, nb.min_height, nb.max_height, 'Height(cm)'));\n errors.push(...checkRange(data.waist_cm, nb.min_waist, nb.max_waist, 'Waist(cm)'));\n errors.push(...checkRange(data.shoe_size_eu, nb.min_shoe, nb.max_shoe, 'Shoe(EU)'));\n errors.push(...checkRange(age, nb.min_age, nb.max_age, 'Age'));\n}\n\n// \u2500\u2500 PREFER NOT TO SAY \u2192 AGE CHECK ONLY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nelse if (gender === 'Prefer Not to Say') {\n const min_age = Math.min(\n f.min_age || 99, m.min_age || 99, nb.min_age || 99\n );\n const max_age = Math.max(\n f.max_age || 0, m.max_age || 0, nb.max_age || 0\n );\n errors.push(...checkRange(age, min_age, max_age, 'Age'));\n}\n\n// \u2500\u2500 RESULT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst gender_not_specified = gender === 'Prefer Not to Say';\n\nif (errors.length > 0) {\n return [{\n json: {\n ...data,\n hard_filter_passed: false,\n hard_filter_fail_reason: errors.join(' | '),\n application_stage: 'hard_filter_fail',\n }\n }];\n}\n\nreturn [{\n json: {\n ...data,\n hard_filter_passed: true,\n hard_filter_fail_reason: gender_not_specified ? 'Gender Not Specified' : null,\n application_stage: 'screening',\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4256,
-464
],
"id": "2db9870f-34f3-40b9-a319-a7e641bea444",
"name": "Code: Hard Filter Check"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "b3fd4190-2893-412d-b611-9e5dd35703a2",
"leftValue": "={{ $json.hard_filter_passed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
4480,
-464
],
"id": "da59ab88-5a9b-4aed-8ccc-df5d41e8d5a2",
"name": "IF: Hard Filter Gate",
"notesInFlow": true,
"retryOnFail": true,
"notes": "TRUE = passed all hard filters.\nContinue to Zone 3 Gemini scoring.\nFALSE = failed one or more filters.\nUpdate Supabase with fail reason, stop."
},
{
"parameters": {
"content": "# ZONE 2\n**Checks All hard filters set by the Modelling Agency**\n",
"height": 1072,
"width": 848
},
"type": "n8n-nodes-base.stickyNote",
"position": [
4624,
-1008
],
"typeVersion": 1,
"id": "37d2d463-9eb1-422d-ac8d-1a8f9df4a704",
"name": "Sticky Note1"
},
{
"parameters": {
"operation": "update",
"tableId": "intake",
"filters": {
"conditions": [
{
"keyName": "intake_id",
"condition": "eq",
"keyValue": "={{ $('Write Initial Supabase Row').first().json.intake_id }}"
}
]
},
"fieldsUi": {
"fieldValues": [
{
"fieldId": "application_stage",
"fieldValue": "hard_filter_fail"
},
{
"fieldId": "hard_filter_fail_reason",
"fieldValue": "={{ $json.hard_filter_fail_reason }}"
},
{
"fieldId": "status",
"fieldValue": "inactive"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
4704,
-368
],
"id": "24dcfcb3-d896-4c28-8e54-32128f4b7033",
"name": "Supabase: Update Hard Filter Fail",
"retryOnFail": true,
"notesInFlow": true,
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "Updates intake row with hard filter fail reason. \nSets stage to hard_filter_fail, status inactive."
},
{
"parameters": {
"errorMessage": "\"Hard filter failed : Application Rejected.\""
},
"type": "n8n-nodes-base.stopAndError",
"typeVersion": 1,
"position": [
4928,
-368
],
"id": "8ccb74b1-1d66-40d4-8af0-a8e238512a8d",
"name": "Stop: Hard Filter Fail"
},
{
"parameters": {
"method": "POST",
"url": "https://spx-r2-signer.spexai.workers.dev/sign",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"r2_object_key\": \"{{ $('Supabase: Write media_forensics Row').item.json.r2_object_key }}\",\n \"expiry_seconds\": 1209600\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
3808,
-464
],
"id": "225a9d8a-802b-4ffd-b3a6-66666332d64d",
"name": "HTTP: Sign R2 URL",
"notesInFlow": true,
"retryOnFail": true,
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"notes": "Calls Cloudflare Worker to generate 7-day presigned GET URL for R2 photo. "
},
{
"parameters": {
"operation": "update",
"tableId": "media_forensics",
"filters": {
"conditions": [
{
"keyName": "id",
"condition": "eq",
"keyValue": "={{ $('Supabase: Write media_forensics Row').item.json.id }}"
}
]
},
"fieldsUi": {
"fieldValues": [
{
"fieldId": "retool_signed_url",
"fieldValue": "={{ $json.retool_signed_url }}"
},
{
"fieldId": "retool_url_expires_at",
"fieldValue": "={{ $json.retool_url_expires_at }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
4032,
-464
],
"id": "19aeaf2d-70d0-47d8-a525-b4a592558d10",
"name": "Supabase: Update Signed URL",
"retryOnFail": true,
"notesInFlow": true,
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "Writes retool_signed_url and retool_url_expires_at to media_forensics."
},
{
"parameters": {
"operation": "update",
"tableId": "intake",
"filters": {
"conditions": [
{
"keyName": "intake_id",
"condition": "eq",
"keyValue": "={{ $('Write Initial Supabase Row').item.json.intake_id }}"
}
]
},
"fieldsUi": {
"fieldValues": [
{
"fieldId": "batch_processing_status",
"fieldValue": "queued"
},
{
"fieldId": "application_stage",
"fieldValue": "processing"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
4704,
-560
],
"id": "ab0cdb2a-b7dc-473b-ae29-4e0a6db43b8a",
"name": "Supabase: Update Queued Status",
"notesInFlow": true,
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "The application is queued to next stage"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "pTl0Nkj8ymaZIXQ1",
"mode": "list",
"cachedResultUrl": "/workflow/pTl0Nkj8ymaZIXQ1",
"cachedResultName": "intake-scoring-photo-check"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"intake_id": "={{ $('Write Initial Supabase Row').item.json.intake_id }}"
},
"matchingColumns": [
"intake_id"
],
"schema": [
{
"id": "intake_id",
"displayName": "intake_id",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {
"waitForSubWorkflow": false
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"position": [
4928,
-560
],
"id": "c4f0ed6a-5e94-47ea-8682-64f915d70422",
"name": "Call 'noir-collective-france-intake-scoring-photo-check'",
"notesInFlow": true,
"onError": "continueRegularOutput",
"notes": "Async trigger to \"noir-collective-france-intake-scoring-photo-check\"\nPasses intake_id only. Does not wait for completion.\nIf trigger fails, cron picks up queued record within 10 minutes."
}
],
"connections": {
"Form Intake Trigger": {
"main": [
[
{
"node": "ACK : Submission Received",
"type": "main",
"index": 0
}
],
[
{
"node": "ACK : Unauthorized",
"type": "main",
"index": 0
}
]
]
},
"ACK : Unauthorized": {
"main": [
[]
]
},
"ACK : Submission Received": {
"main": [
[
{
"node": "Read Agency Slug",
"type": "main",
"index": 0
}
]
]
},
"Read Agency Slug": {
"main": [
[
{
"node": "Code: Parse & Validate Form Data",
"type": "main",
"index": 0
}
]
]
},
"Code: Parse & Validate Form Data": {
"main": [
[
{
"node": "Duplicate Application Check",
"type": "main",
"index": 0
}
]
]
},
"Duplicate Application Check": {
"main": [
[
{
"node": "IF: New or Returning Applicant",
"type": "main",
"index": 0
}
]
]
},
"IF: New or Returning Applicant": {
"main": [
[
{
"node": "IF: Consent Gate",
"type": "main",
"index": 0
}
],
[
{
"node": "Switch: Cooldown Check",
"type": "main",
"index": 0
}
]
]
},
"IF: Consent Gate": {
"main": [
[
{
"node": "IF: Minor Flag Check",
"type": "main",
"index": 0
}
],
[
{
"node": "Stop: Consent Missing",
"type": "main",
"index": 0
}
]
]
},
"IF: Minor Flag Check": {
"main": [
[
{
"node": "IF: Guardian Consent Check",
"type": "main",
"index": 0
}
],
[
{
"node": "Code: Generate Batch ID",
"type": "main",
"index": 0
}
]
]
},
"IF: Guardian Consent Check": {
"main": [
[
{
"node": "Code: Generate Batch ID",
"type": "main",
"index": 0
}
],
[
{
"node": "Stop: Minor \u2014 Guardian Check Failed",
"type": "main",
"index": 0
}
]
]
},
"Code: Generate Batch ID": {
"main": [
[
{
"node": "Write Initial Supabase Row",
"type": "main",
"index": 0
}
]
]
},
"Switch: Cooldown Check": {
"main": [
[
{
"node": "Stop: Cooldown Active",
"type": "main",
"index": 0
}
],
[
{
"node": "Code: Set Reapplication Flag",
"type": "main",
"index": 0
}
]
]
},
"Code: Set Reapplication Flag": {
"main": [
[
{
"node": "IF: Consent Gate",
"type": "main",
"index": 0
}
]
]
},
"Write Initial Supabase Row": {
"main": [
[
{
"node": "Code: Flatten Photos for Upload",
"type": "main",
"index": 0
}
]
]
},
"Code: Flatten Photos for Upload": {
"main": [
[
{
"node": "HTTP: Download Photo from Tally",
"type": "main",
"index": 0
}
]
]
},
"HTTP: Download Photo from Tally": {
"main": [
[
{
"node": "S3: Upload Photo to R2",
"type": "main",
"index": 0
}
],
[
{
"node": "HTTP Error Branch",
"type": "main",
"index": 0
}
]
]
},
"S3: Upload Photo to R2": {
"main": [
[
{
"node": "Supabase: Write media_forensics Row",
"type": "main",
"index": 0
}
],
[
{
"node": "S3 Error Branch",
"type": "main",
"index": 0
}
]
]
},
"Supabase: Write media_forensics Row": {
"main": [
[
{
"node": "HTTP: Sign R2 URL",
"type": "main",
"index": 0
}
]
]
},
"Code: Hard Filter Check": {
"main": [
[
{
"node": "IF: Hard Filter Gate",
"type": "main",
"index": 0
}
]
]
},
"IF: Hard Filter Gate": {
"main": [
[
{
"node": "Supabase: Update Queued Status",
"type": "main",
"index": 0
}
],
[
{
"node": "Supabase: Update Hard Filter Fail",
"type": "main",
"index": 0
}
]
]
},
"Supabase: Update Hard Filter Fail": {
"main": [
[
{
"node": "Stop: Hard Filter Fail",
"type": "main",
"index": 0
}
]
]
},
"HTTP: Sign R2 URL": {
"main": [
[
{
"node": "Supabase: Update Signed URL",
"type": "main",
"index": 0
}
]
]
},
"Supabase: Update Signed URL": {
"main": [
[
{
"node": "Code: Hard Filter Check",
"type": "main",
"index": 0
}
]
]
},
"Supabase: Update Queued Status": {
"main": [
[
{
"node": "Call 'noir-collective-france-intake-scoring-photo-check'",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"versionId": "4353f8f1-c408-4165-8044-1f284321fe60",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "HGsFujNHZz5AAkXa",
"tags": [
{
"updatedAt": "2026-04-07T05:54:22.284Z",
"createdAt": "2026-04-07T05:54:22.284Z",
"id": "1xyV3F0vU5tED40A",
"name": "intake"
},
{
"updatedAt": "2026-04-07T05:54:12.040Z",
"createdAt": "2026-04-07T05:54:12.040Z",
"id": "3ctqgxjHRVbu8uPR",
"name": "noir-test"
},
{
"updatedAt": "2026-04-07T05:54:41.309Z",
"createdAt": "2026-04-07T05:54:41.309Z",
"id": "617SY1s5iJZzmnko",
"name": "noir-collective-france"
},
{
"updatedAt": "2026-04-07T05:54:33.816Z",
"createdAt": "2026-04-07T05:54:33.816Z",
"id": "G7U2cDrrdAaCu0BL",
"name": "talent-prism"
},
{
"updatedAt": "2026-04-07T05:54:15.456Z",
"createdAt": "2026-04-07T05:54:15.456Z",
"id": "U5Z5J0HkLxUQA5hK",
"name": "prototype"
},
{
"updatedAt": "2026-04-07T06:57:56.832Z",
"createdAt": "2026-04-07T06:57:56.832Z",
"id": "wAJsiiKAX1HZZKGW",
"name": "reference"
},
{
"updatedAt": "2026-04-07T06:58:06.901Z",
"createdAt": "2026-04-07T06:58:06.901Z",
"id": "xzkRjFgbGBkSMd3E",
"name": "demo"
}
]
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
airtableTokenApihttpHeaderAuths3supabaseApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
01-intake-storage. Uses airtable, supabase, stopAndError, httpRequest. Webhook trigger; 33 nodes.
Source: https://github.com/dev-spai/spexecute-talent-prism-osr/blob/main/workflows/01-intake-storage.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
2. Refresh Pipedrive tokens. Uses stopAndError, stickyNote, supabase, httpRequest. Webhook trigger; 29 nodes.
This workflow provides an OAuth 2.0 auth token refresh process for better control. Developers can utilize it as an alternative to n8n's built-in OAuth flow to achieve improved control and visibility.
Buildnbloom - Typeform to Tier 1 Call. Uses supabase, airtable, httpRequest. Webhook trigger; 17 nodes.
This solution enables you to manage all your Notion and Todoist tasks from different workspaces as well as your calendar events in a single place. This is 2 way sync with partial support for recurring
Notion to Clockify Sync Template. Uses scheduleTrigger, clockify, compareDatasets, stopAndError. Webhook trigger; 68 nodes.