{ "kind": "open-flow-workflow", "version": 1, "name": "Fill Airtable SEO Metadata from Webpages", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_990b527e5590e00b": { "name": "Fill missing metadata", "kind": "start", "inputDefs": [] }, "n_1f64b79fff96a8a7": { "inputs": { "baseId": { "kind": "value", "value": "YOUR_AIRTABLE_BASE_ID" }, "tableIdOrName": { "kind": "value", "value": "YOUR_AIRTABLE_TABLE_ID" }, "filterByFormula": { "kind": "value", "value": "AND(url != \"\", {title tag} = \"\", {meta desc} = \"\")" }, "maxRecords": { "kind": "value", "value": 10 }, "pageSize": { "kind": "value", "value": 10 } }, "name": "Find records missing metadata", "kind": "task", "task": { "inputs": [ { "description": "Base ID in the format appXXXXXXXXXXXXXX.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Base ID in the format appXXXXXXXXXXXXXX." }, "nullable": false, "handle": "baseId" }, { "description": "Table ID or table name accepted by the Airtable path parameter.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Table ID or table name accepted by the Airtable path parameter." }, "nullable": false, "handle": "tableIdOrName" }, { "description": "View name or view ID used by Airtable to filter and sort results.", "jsonSchema": { "type": "string", "minLength": 1, "description": "View name or view ID used by Airtable to filter and sort results." }, "nullable": true, "handle": "view", "value": null }, { "description": "Field names or field IDs to include in the Airtable response.", "jsonSchema": { "type": "array", "items": { "type": "string", "minLength": 1, "description": "An array item." }, "minItems": 1, "description": "Field names or field IDs to include in the Airtable response." }, "nullable": true, "handle": "fields", "value": null }, { "description": "Sort rules applied by Airtable in order.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "field": { "type": "string", "minLength": 1, "description": "Field name or field ID used by Airtable for sorting." }, "direction": { "type": "string", "enum": [ "asc", "desc" ], "description": "Sort direction accepted by Airtable." } }, "required": [ "field" ], "additionalProperties": false, "description": "Sort rule accepted by the Airtable list records endpoint." }, "minItems": 1, "description": "Sort rules applied by Airtable in order." }, "nullable": true, "handle": "sort", "value": null }, { "description": "Formula string evaluated by Airtable to filter matching records.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Formula string evaluated by Airtable to filter matching records." }, "nullable": true, "handle": "filterByFormula", "value": null }, { "description": "Maximum total number of records to return before Airtable stops pagination.", "jsonSchema": { "type": "integer", "minimum": 1, "description": "Maximum total number of records to return before Airtable stops pagination." }, "nullable": true, "handle": "maxRecords", "value": null }, { "description": "Number of records to return per page.", "jsonSchema": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Number of records to return per page." }, "nullable": true, "handle": "pageSize", "value": null }, { "description": "Opaque pagination cursor returned by a previous Airtable response.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Opaque pagination cursor returned by a previous Airtable response." }, "nullable": true, "handle": "offset", "value": null }, { "description": "Cell format accepted by Airtable for read operations.", "jsonSchema": { "type": "string", "enum": [ "json", "string" ], "description": "Cell format accepted by Airtable for read operations." }, "nullable": true, "handle": "cellFormat", "value": null }, { "description": "Timezone string sent to Airtable when cellFormat is string.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Timezone string sent to Airtable when cellFormat is string." }, "nullable": true, "handle": "timeZone", "value": null }, { "description": "User locale sent to Airtable when cellFormat is string.", "jsonSchema": { "type": "string", "minLength": 1, "description": "User locale sent to Airtable when cellFormat is string." }, "nullable": true, "handle": "userLocale", "value": null }, { "description": "Whether Airtable should return field IDs instead of field names in record objects.", "jsonSchema": { "type": "boolean", "description": "Whether Airtable should return field IDs instead of field names in record objects." }, "nullable": true, "handle": "returnFieldsByFieldId", "value": null }, { "description": "Whether Airtable should return date dependency metadata for linked record fields.", "jsonSchema": { "type": "boolean", "description": "Whether Airtable should return date dependency metadata for linked record fields." }, "nullable": true, "handle": "includeDateDependencyMetadata", "value": null }, { "description": "Record metadata fields to include in Airtable list records responses.", "jsonSchema": { "type": "array", "items": { "const": "commentCount", "description": "An array item." }, "minItems": 1, "description": "Record metadata fields to include in Airtable list records responses." }, "nullable": true, "handle": "recordMetadata", "value": null } ], "outputs": [ { "description": "Records returned by Airtable.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Record ID." }, "createdTime": { "type": "string", "description": "Record creation timestamp returned by Airtable." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Record fields keyed by field name or field ID." }, "commentCount": { "type": "integer", "description": "Comment count returned by Airtable when enabled on the endpoint." } }, "required": [ "id", "fields" ], "additionalProperties": true, "description": "Airtable record." }, "description": "Records returned by Airtable." }, "nullable": false, "handle": "records" }, { "description": "Pagination cursor for the next page of records, or null when unavailable.", "jsonSchema": { "type": [ "string", "null" ], "description": "Pagination cursor for the next page of records, or null when unavailable." }, "nullable": true, "handle": "offset" } ], "name": "Find records missing metadata", "executor": { "kind": "connector", "action": "airtable.list_records" } } }, "n_fcee7128ac91b09f": { "inputs": { "records": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_1f64b79fff96a8a7", "output": "records" } ] }, "previous": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_fcee7128ac91b09f", "output": "state" } ] } }, "name": "Choose next record", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Record ID." }, "createdTime": { "type": "string", "description": "Record creation timestamp returned by Airtable." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Record fields keyed by field name or field ID." }, "commentCount": { "type": "integer", "description": "Comment count returned by Airtable when enabled on the endpoint." } }, "required": [ "id", "fields" ], "additionalProperties": true, "description": "Airtable record." }, "description": "Records returned by Airtable." }, "nullable": false, "handle": "records" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": true, "handle": "previous" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "string", "format": "uri", "description": "The public URL to extract data from with ScrapingBee." }, "nullable": false, "handle": "url" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "id" } ], "name": "Choose next record", "moduleId": "n_9b6cc755b2512b07", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_83d0a9a1ac9a2da6": { "inputs": {}, "name": "More records?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "yes", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_fcee7128ac91b09f", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_e3f45c5228dd15ca": { "inputs": { "url": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_fcee7128ac91b09f", "output": "url" } ] }, "extractRules": { "kind": "value", "value": { "title": { "selector": "title", "output": "text", "clean": false }, "description": { "selector": "meta[name=\"description\"]", "output": "@content" } } }, "renderJs": { "kind": "value", "value": false }, "transparentStatusCode": { "kind": "value", "value": true } }, "name": "Extract page metadata", "kind": "task", "task": { "inputs": [ { "description": "The public URL to extract data from with ScrapingBee.", "jsonSchema": { "type": "string", "format": "uri", "description": "The public URL to extract data from with ScrapingBee." }, "nullable": false, "handle": "url" }, { "description": "The extraction rules object serialized into the extract_rules query parameter.", "jsonSchema": { "type": "object", "additionalProperties": { "description": "A JSON-compatible value used in ScrapingBee extraction rules or responses." }, "description": "The extraction rules object serialized into the extract_rules query parameter." }, "nullable": false, "handle": "extractRules" }, { "description": "Whether ScrapingBee should render JavaScript before returning the page.", "jsonSchema": { "type": "boolean", "description": "Whether ScrapingBee should render JavaScript before returning the page." }, "nullable": true, "handle": "renderJs", "value": null }, { "description": "How many milliseconds ScrapingBee should wait before returning the page.", "jsonSchema": { "type": "integer", "minimum": 0, "description": "How many milliseconds ScrapingBee should wait before returning the page." }, "nullable": true, "handle": "waitMs", "value": null }, { "description": "The CSS selector ScrapingBee should wait for before returning the page.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The CSS selector ScrapingBee should wait for before returning the page." }, "nullable": true, "handle": "waitFor", "value": null }, { "description": "The device preset used for the request.", "jsonSchema": { "type": "string", "enum": [ "desktop", "mobile" ], "description": "The device preset used for the request." }, "nullable": true, "handle": "device", "value": null }, { "description": "Whether ScrapingBee should block ads on the page.", "jsonSchema": { "type": "boolean", "description": "Whether ScrapingBee should block ads on the page." }, "nullable": true, "handle": "blockAds", "value": null }, { "description": "Whether ScrapingBee should block images and CSS resources.", "jsonSchema": { "type": "boolean", "description": "Whether ScrapingBee should block images and CSS resources." }, "nullable": true, "handle": "blockResources", "value": null }, { "description": "The two-letter country code used for request geolocation.", "jsonSchema": { "type": "string", "minLength": 2, "maxLength": 2, "description": "The two-letter country code used for request geolocation." }, "nullable": true, "handle": "countryCode", "value": null }, { "description": "Whether ScrapingBee should use premium proxy routing.", "jsonSchema": { "type": "boolean", "description": "Whether ScrapingBee should use premium proxy routing." }, "nullable": true, "handle": "premiumProxy", "value": null }, { "description": "Whether ScrapingBee should use stealth proxy routing.", "jsonSchema": { "type": "boolean", "description": "Whether ScrapingBee should use stealth proxy routing." }, "nullable": true, "handle": "stealthProxy", "value": null }, { "description": "Whether ScrapingBee should return the target page status code transparently.", "jsonSchema": { "type": "boolean", "description": "Whether ScrapingBee should return the target page status code transparently." }, "nullable": true, "handle": "transparentStatusCode", "value": null }, { "description": "How many times ScrapingBee should retry the request on failure.", "jsonSchema": { "type": "integer", "exclusiveMinimum": 0, "description": "How many times ScrapingBee should retry the request on failure." }, "nullable": true, "handle": "retry", "value": null } ], "outputs": [ { "description": "The structured data object returned by ScrapingBee extract_rules.", "jsonSchema": { "type": "object", "additionalProperties": { "description": "A JSON-compatible value used in ScrapingBee extraction rules or responses." }, "description": "The structured data object returned by ScrapingBee extract_rules." }, "nullable": false, "handle": "data" }, { "description": "The HTTP status code returned by ScrapingBee.", "jsonSchema": { "type": "integer", "description": "The HTTP status code returned by ScrapingBee." }, "nullable": false, "handle": "statusCode" }, { "description": "The final resolved URL reported by ScrapingBee.", "jsonSchema": { "type": "string", "description": "The final resolved URL reported by ScrapingBee." }, "nullable": true, "handle": "resolvedUrl" }, { "description": "The request credit cost reported by ScrapingBee.", "jsonSchema": { "type": "number", "description": "The request credit cost reported by ScrapingBee." }, "nullable": true, "handle": "creditCost" } ], "name": "Extract page metadata", "executor": { "kind": "connector", "action": "scrapingbee.extract_data" } } }, "n_a97b24351c813cd0": { "inputs": { "data": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e3f45c5228dd15ca", "output": "data" } ] }, "statusCode": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e3f45c5228dd15ca", "output": "statusCode" } ] }, "id": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_fcee7128ac91b09f", "output": "id" } ] } }, "name": "Prepare record update", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": { "description": "A JSON-compatible value used in ScrapingBee extraction rules or responses." }, "description": "The structured data object returned by ScrapingBee extract_rules." }, "nullable": false, "handle": "data" }, { "jsonSchema": { "type": "integer", "description": "The HTTP status code returned by ScrapingBee." }, "nullable": false, "handle": "statusCode" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "id" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "minLength": 1, "description": "Record ID in the format recXXXXXXXXXXXXXX." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Field values to write into the existing Airtable record." } }, "required": [ "id", "fields" ], "additionalProperties": false, "description": "Record payload used when updating Airtable records." }, "minItems": 1, "maxItems": 10, "description": "Records to update in the Airtable table." }, "nullable": false, "handle": "records" } ], "name": "Prepare record update", "moduleId": "n_d4cf6afc772a63c0", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_1c27d3063b2d984b": { "inputs": { "baseId": { "kind": "value", "value": "YOUR_AIRTABLE_BASE_ID" }, "tableIdOrName": { "kind": "value", "value": "YOUR_AIRTABLE_TABLE_ID" }, "records": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_a97b24351c813cd0", "output": "records" } ] }, "typecast": { "kind": "value", "value": false } }, "name": "Update matching record", "kind": "task", "task": { "inputs": [ { "description": "Base ID in the format appXXXXXXXXXXXXXX.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Base ID in the format appXXXXXXXXXXXXXX." }, "nullable": false, "handle": "baseId" }, { "description": "Table ID or table name accepted by the Airtable path parameter.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Table ID or table name accepted by the Airtable path parameter." }, "nullable": false, "handle": "tableIdOrName" }, { "description": "Records to update in the Airtable table.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "minLength": 1, "description": "Record ID in the format recXXXXXXXXXXXXXX." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Field values to write into the existing Airtable record." } }, "required": [ "id", "fields" ], "additionalProperties": false, "description": "Record payload used when updating Airtable records." }, "minItems": 1, "maxItems": 10, "description": "Records to update in the Airtable table." }, "nullable": false, "handle": "records" }, { "description": "Whether Airtable should coerce incoming values to compatible field types.", "jsonSchema": { "type": "boolean", "description": "Whether Airtable should coerce incoming values to compatible field types." }, "nullable": true, "handle": "typecast", "value": null }, { "description": "Whether Airtable should return field IDs instead of field names in record objects.", "jsonSchema": { "type": "boolean", "description": "Whether Airtable should return field IDs instead of field names in record objects." }, "nullable": true, "handle": "returnFieldsByFieldId", "value": null } ], "outputs": [ { "description": "Records returned by Airtable after update.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Record ID." }, "createdTime": { "type": "string", "description": "Record creation timestamp returned by Airtable." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Record fields keyed by field name or field ID." }, "commentCount": { "type": "integer", "description": "Comment count returned by Airtable when enabled on the endpoint." } }, "required": [ "id", "fields" ], "additionalProperties": true, "description": "Airtable record." }, "description": "Records returned by Airtable after update." }, "nullable": false, "handle": "records" } ], "name": "Update matching record", "executor": { "kind": "connector", "action": "airtable.update_records" } } }, "n_5a87f1a72ee2ad61": { "inputs": { "records": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_1c27d3063b2d984b", "output": "records" } ] }, "id": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_fcee7128ac91b09f", "output": "id" } ] } }, "name": "Confirm record update", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Record ID." }, "createdTime": { "type": "string", "description": "Record creation timestamp returned by Airtable." }, "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Record fields keyed by field name or field ID." }, "commentCount": { "type": "integer", "description": "Comment count returned by Airtable when enabled on the endpoint." } }, "required": [ "id", "fields" ], "additionalProperties": true, "description": "Airtable record." }, "description": "Records returned by Airtable after update." }, "nullable": false, "handle": "records" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "id" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "ok" } ], "name": "Confirm record update", "moduleId": "n_560822c50ca38b78", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } } }, "edges": [ { "source": "n_990b527e5590e00b", "target": "n_1f64b79fff96a8a7" }, { "source": "n_1f64b79fff96a8a7", "target": "n_fcee7128ac91b09f" }, { "source": "n_fcee7128ac91b09f", "target": "n_83d0a9a1ac9a2da6" }, { "source": "n_83d0a9a1ac9a2da6", "target": "n_e3f45c5228dd15ca", "sourceHandle": "yes" }, { "source": "n_e3f45c5228dd15ca", "target": "n_a97b24351c813cd0" }, { "source": "n_a97b24351c813cd0", "target": "n_1c27d3063b2d984b" }, { "source": "n_1c27d3063b2d984b", "target": "n_5a87f1a72ee2ad61" }, { "source": "n_5a87f1a72ee2ad61", "target": "n_fcee7128ac91b09f" } ] } }, "modules": { "n_9b6cc755b2512b07": { "name": "Choose next record", "imports": [], "source": "export default function main({records,previous}) {\n if (!Array.isArray(records)) throw Error('Invalid Airtable records');\n const i=previous == null ? 0 : previous.index+1;\n if(!Number.isInteger(i)||i<0||i>records.length)throw Error('Invalid position');\n if(i===records.length)return {state:{index:i},more:false,url:'https://example.com',id:'none'};\n const r=records[i],url=r?.fields?.url;\n if(typeof r?.id!=='string'||!r.id||typeof url!=='string'||!/^https?:\\/\\//i.test(url))throw Error('Record needs an ID and public HTTP URL');\n return {state:{index:i},more:true,url,id:r.id};\n}\n" }, "n_d4cf6afc772a63c0": { "name": "Prepare record update", "imports": [], "source": "export default function main({data,id,statusCode}) {\n if(statusCode<200 || statusCode>=300) throw Error('Page request failed');\n if(!data||typeof data.title!=='string'||typeof data.description!=='string')throw Error('Page must provide a title and meta description');\n return {records:[{id,fields:{'title tag':data.title,'meta desc':data.description}}]};\n}\n" }, "n_560822c50ca38b78": { "name": "Confirm record update", "imports": [], "source": "export default function main({records,id}) {\n if(!Array.isArray(records)||records.length!==1||records[0]?.id!==id)throw Error('Airtable update did not confirm the matching record');\n return {ok:true};\n}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }