{ "kind": "open-flow-workflow", "version": 1, "name": "Save New X Search Results to Airtable", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_15e46cf62d6e0800": { "name": "Search for new posts", "kind": "start", "inputDefs": [] }, "n_02b4ae0c657e7862": { "inputs": { "query": { "kind": "value", "value": "verstappen" }, "maxResults": { "kind": "value", "value": 100 }, "tweetFields": { "kind": "value", "value": [ "author_id", "created_at", "public_metrics", "in_reply_to_user_id" ] }, "expansions": { "kind": "value", "value": [ "author_id", "in_reply_to_user_id" ] }, "userFields": { "kind": "value", "value": [ "username" ] } }, "name": "Search recent X posts", "kind": "task", "task": { "inputs": [ { "description": "The X search query to execute.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The X search query to execute." }, "nullable": false, "handle": "query" }, { "description": "The oldest UTC timestamp to include, in ISO 8601 format.", "jsonSchema": { "type": "string", "description": "The oldest UTC timestamp to include, in ISO 8601 format." }, "nullable": true, "handle": "startTime", "value": null }, { "description": "The newest UTC timestamp to include, in ISO 8601 format.", "jsonSchema": { "type": "string", "description": "The newest UTC timestamp to include, in ISO 8601 format." }, "nullable": true, "handle": "endTime", "value": null }, { "description": "Return Tweets with IDs greater than this Tweet ID.", "jsonSchema": { "type": "string", "description": "Return Tweets with IDs greater than this Tweet ID." }, "nullable": true, "handle": "sinceId", "value": null }, { "description": "Return Tweets with IDs less than this Tweet ID.", "jsonSchema": { "type": "string", "description": "Return Tweets with IDs less than this Tweet ID." }, "nullable": true, "handle": "untilId", "value": null }, { "description": "The maximum number of Tweets to return in a page.", "jsonSchema": { "type": "integer", "minimum": 10, "maximum": 100, "description": "The maximum number of Tweets to return in a page." }, "nullable": true, "handle": "maxResults", "value": null }, { "description": "The next_token from a previous X API response.", "jsonSchema": { "type": "string", "description": "The next_token from a previous X API response." }, "nullable": true, "handle": "nextToken", "value": null }, { "description": "The pagination token from a previous X API response.", "jsonSchema": { "type": "string", "description": "The pagination token from a previous X API response." }, "nullable": true, "handle": "paginationToken", "value": null }, { "description": "The order used to rank Tweets in the response.", "jsonSchema": { "type": "string", "enum": [ "recency", "relevancy" ], "description": "The order used to rank Tweets in the response." }, "nullable": true, "handle": "sortOrder", "value": null }, { "description": "The expansions to include in the X API response.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "A string value." }, "description": "The expansions to include in the X API response." }, "nullable": true, "handle": "expansions", "value": null }, { "description": "The poll fields to request from the X API.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "A string value." }, "description": "The poll fields to request from the X API." }, "nullable": true, "handle": "pollFields", "value": null }, { "description": "The user fields to request from the X API.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "A string value." }, "description": "The user fields to request from the X API." }, "nullable": true, "handle": "userFields", "value": null }, { "description": "The media fields to request from the X API.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "A string value." }, "description": "The media fields to request from the X API." }, "nullable": true, "handle": "mediaFields", "value": null }, { "description": "The place fields to request from the X API.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "A string value." }, "description": "The place fields to request from the X API." }, "nullable": true, "handle": "placeFields", "value": null }, { "description": "The tweet fields to request from the X API.", "jsonSchema": { "type": "array", "items": { "type": "string", "description": "A string value." }, "description": "The tweet fields to request from the X API." }, "nullable": true, "handle": "tweetFields", "value": null } ], "outputs": [ { "description": "The normalized Tweets returned by the X API.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "The unique identifier of the Tweet." }, "text": { "type": "string", "description": "The text content of the Tweet." }, "authorId": { "type": [ "string", "null" ], "description": "The author user ID of the Tweet." }, "createdAt": { "type": [ "string", "null" ], "description": "The Tweet creation time." }, "lang": { "type": [ "string", "null" ], "description": "The language code of the Tweet." }, "publicMetrics": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The public metrics of the Tweet." }, "raw": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The raw Tweet object returned by the X API." } }, "required": [ "id", "text", "raw" ], "additionalProperties": false, "description": "A normalized Tweet item." }, "description": "The normalized Tweets returned by the X API." }, "nullable": false, "handle": "posts" }, { "description": "The expanded objects returned by the X API, when requested.", "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The expanded objects returned by the X API, when requested." }, "nullable": true, "handle": "includes" }, { "description": "The normalized pagination metadata.", "jsonSchema": { "type": "object", "properties": { "resultCount": { "type": "integer", "description": "The number of results returned in this page." }, "nextToken": { "type": [ "string", "null" ], "description": "The token for fetching the next page of results." }, "previousToken": { "type": [ "string", "null" ], "description": "The token for fetching the previous page of results." }, "newestId": { "type": [ "string", "null" ], "description": "The newest Tweet ID in this page." }, "oldestId": { "type": [ "string", "null" ], "description": "The oldest Tweet ID in this page." }, "raw": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The raw metadata object returned by the X API." } }, "required": [ "resultCount", "raw" ], "additionalProperties": false, "description": "The normalized pagination metadata." }, "nullable": true, "handle": "meta" } ], "name": "Search recent X posts", "executor": { "kind": "connector", "action": "twitter.recent_search" } } }, "n_5bb00f962326847d": { "inputs": { "baseId": { "kind": "value", "value": "YOUR_AIRTABLE_BASE_ID" }, "tableIdOrName": { "kind": "value", "value": "YOUR_AIRTABLE_TABLE_ID" }, "pageSize": { "kind": "value", "value": 100 }, "offset": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7a910d96bee993a6", "output": "offset" } ] } }, "name": "Read existing Airtable records", "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": "Read existing Airtable records", "executor": { "kind": "connector", "action": "airtable.list_records" } } }, "n_7a910d96bee993a6": { "inputs": { "records": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_5bb00f962326847d", "output": "records" } ] }, "offset": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_5bb00f962326847d", "output": "offset" } ] }, "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7a910d96bee993a6", "output": "state" } ] } }, "name": "Collect existing records", "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": [ "string", "null" ], "description": "Pagination cursor for the next page of records, or null when unavailable." }, "nullable": true, "handle": "offset" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": true, "handle": "previousState" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "string", "minLength": 1 }, "nullable": false, "handle": "offset" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" } ], "name": "Collect existing records", "moduleId": "n_9cac00145954738e", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_929f7bdaf6eed20a": { "inputs": {}, "name": "More Airtable pages?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_7a910d96bee993a6", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_befecfd237955a9a": { "inputs": { "posts": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_02b4ae0c657e7862", "output": "posts" } ] }, "includes": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_02b4ae0c657e7862", "output": "includes" } ] }, "pageState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7a910d96bee993a6", "output": "state" } ] } }, "name": "Find new posts", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "The unique identifier of the Tweet." }, "text": { "type": "string", "description": "The text content of the Tweet." }, "authorId": { "type": [ "string", "null" ], "description": "The author user ID of the Tweet." }, "createdAt": { "type": [ "string", "null" ], "description": "The Tweet creation time." }, "lang": { "type": [ "string", "null" ], "description": "The language code of the Tweet." }, "publicMetrics": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The public metrics of the Tweet." }, "raw": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The raw Tweet object returned by the X API." } }, "required": [ "id", "text", "raw" ], "additionalProperties": false, "description": "A normalized Tweet item." }, "description": "The normalized Tweets returned by the X API." }, "nullable": false, "handle": "posts" }, { "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "The expanded objects returned by the X API, when requested." }, "nullable": true, "handle": "includes" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "pageState" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "pending" } ], "name": "Find new posts", "moduleId": "n_d833331532b39ed0", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_2ccc856eb4af4e38": { "inputs": { "pending": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_befecfd237955a9a", "output": "pending" } ] }, "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2ccc856eb4af4e38", "output": "state" } ] } }, "name": "Prepare next batch", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "pending" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": true, "handle": "previousState" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Field values for the new Airtable record." } }, "required": [ "fields" ], "additionalProperties": false, "description": "Record payload used when creating Airtable records." }, "minItems": 1, "maxItems": 10, "description": "Records to create in the Airtable table." }, "nullable": false, "handle": "records" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "name": "Prepare next batch", "moduleId": "n_0bac8a93ae9bd8b9", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_081a5929a4987fdb": { "inputs": {}, "name": "More posts to save?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_2ccc856eb4af4e38", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_9561db7cd5e441a2": { "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_2ccc856eb4af4e38", "output": "records" } ] }, "typecast": { "kind": "value", "value": false } }, "name": "Append new posts", "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 create in the Airtable table.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "fields": { "type": "object", "additionalProperties": { "description": "A record value." }, "description": "Field values for the new Airtable record." } }, "required": [ "fields" ], "additionalProperties": false, "description": "Record payload used when creating Airtable records." }, "minItems": 1, "maxItems": 10, "description": "Records to create 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 created 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 created by Airtable." }, "nullable": false, "handle": "records" } ], "name": "Append new posts", "executor": { "kind": "connector", "action": "airtable.create_records" } } }, "n_7e958d88cbc3f808": { "name": "Saved post count", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" } ], "outputMappings": { "count": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_2ccc856eb4af4e38", "output": "count" } ] } } } }, "edges": [ { "source": "n_15e46cf62d6e0800", "target": "n_02b4ae0c657e7862" }, { "source": "n_02b4ae0c657e7862", "target": "n_5bb00f962326847d" }, { "source": "n_5bb00f962326847d", "target": "n_7a910d96bee993a6" }, { "source": "n_7a910d96bee993a6", "target": "n_929f7bdaf6eed20a" }, { "source": "n_929f7bdaf6eed20a", "target": "n_5bb00f962326847d", "sourceHandle": "more" }, { "source": "n_929f7bdaf6eed20a", "target": "n_befecfd237955a9a", "sourceHandle": "otherwise" }, { "source": "n_befecfd237955a9a", "target": "n_2ccc856eb4af4e38" }, { "source": "n_2ccc856eb4af4e38", "target": "n_081a5929a4987fdb" }, { "source": "n_081a5929a4987fdb", "target": "n_9561db7cd5e441a2", "sourceHandle": "more" }, { "source": "n_9561db7cd5e441a2", "target": "n_2ccc856eb4af4e38" }, { "source": "n_081a5929a4987fdb", "target": "n_7e958d88cbc3f808", "sourceHandle": "otherwise" } ] } }, "modules": { "n_9cac00145954738e": { "name": "Collect existing records", "imports": [], "source": "export default function collectImagePages({records,offset,previousState}) {\n if (!Array.isArray(records)) throw new Error('Missing Airtable records');\n const state=previousState ?? {records:[],seen:[]};\n if (!Array.isArray(state.records)||!Array.isArray(state.seen)) throw new Error('Invalid pagination state');\n if (offset!==null && (typeof offset!=='string'||!offset||state.seen.includes(offset))) throw new Error('Invalid or repeated Airtable offset');\n return {state:{records:[...state.records,...records],seen:offset===null?state.seen:[...state.seen,offset]},offset:offset??\"unused\",more:offset!==null};\n}\n" }, "n_d833331532b39ed0": { "name": "Find new posts", "imports": [], "source": "export default function plan({posts,includes,pageState}){const records=pageState?.records;if(!Array.isArray(posts)||!Array.isArray(records))throw Error('Invalid search or Airtable response');const users=new Map((includes?.users??[]).map(u=>[String(u.id),u.username])),known=new Set(records.map(r=>r?.fields?.Tweet_id).filter(x=>x!==undefined&&x!==null).map(String)),pending=[];for(const p of posts){if(typeof p.id!=='string'||typeof p.text!=='string'||typeof p.createdAt!=='string'||typeof p.publicMetrics?.like_count!=='number')throw Error('Missing post ID, text, creation time or like count');if(known.has(p.id))continue;const author=users.get(String(p.authorId));if(typeof author!=='string'||!author)throw Error('Missing expanded post author');const replyId=p.raw?.in_reply_to_user_id,reply=replyId==null?null:users.get(String(replyId));if(replyId!=null&&typeof reply!=='string')throw Error('Missing expanded reply user');pending.push({fields:{Likes:p.publicMetrics?.like_count??0,Tweet:p.text,Tweet_id:p.id,'Tweet URL':'https://twitter.com/'+author+'/status/'+p.id,Author:reply,Time:p.createdAt}})}return{pending}}\n" }, "n_0bac8a93ae9bd8b9": { "name": "Prepare next batch", "imports": [], "source": "export default function next({pending,previousState}){const state=previousState??{index:0};if(previousState)state.index+=10;if(!Array.isArray(pending)||state.index>pending.length+9)throw Error('Invalid write state');return{state,more:state.index