{ "kind": "open-flow-workflow", "version": 1, "name": "Forward X Search Results to Mattermost", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_0f3445d1e486b159": { "name": "Every minute", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "* * * * *", "timezone": "UTC" } ] }, "n_e50fd7cac2a37d8a": { "inputs": { "query": { "kind": "value", "value": "n8n_io" }, "sortOrder": { "kind": "value", "value": "recency" }, "maxResults": { "kind": "value", "value": 50 }, "tweetFields": { "kind": "value", "value": [ "author_id" ] }, "expansions": { "kind": "value", "value": [ "author_id" ] }, "userFields": { "kind": "value", "value": [ "profile_image_url" ] } }, "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_3e4ad16865cfc928": { "inputs": { "posts": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e50fd7cac2a37d8a", "output": "posts" } ] }, "includes": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e50fd7cac2a37d8a", "output": "includes" } ] } }, "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" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "pending" }, { "jsonSchema": { "type": "array", "items": { "type": "string" } }, "nullable": false, "handle": "ids" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "hasResults" } ], "name": "Find new posts", "moduleId": "n_66a6d12b7462473b", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_3da8fde9c7a9ed57": { "inputs": { "pending": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_3e4ad16865cfc928", "output": "pending" } ] }, "previousState": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_3da8fde9c7a9ed57", "output": "state" } ] } }, "name": "Prepare next notification", "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": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "string", "minLength": 1, "description": "The Markdown message body to post." }, "nullable": false, "handle": "message" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "props" } ], "name": "Prepare next notification", "moduleId": "n_c5e0577f692f90fc", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_b949b3de955881f8": { "inputs": {}, "name": "More new posts?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "send", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_3da8fde9c7a9ed57", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_bd63947595e026f1": { "inputs": { "channelId": { "kind": "value", "value": "YOUR_CHANNEL_ID" }, "message": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_3da8fde9c7a9ed57", "output": "message" } ] }, "props": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_3da8fde9c7a9ed57", "output": "props" } ] } }, "name": "Post to Mattermost", "kind": "task", "task": { "inputs": [ { "description": "A Mattermost object ID.", "jsonSchema": { "type": "string", "minLength": 1, "description": "A Mattermost object ID." }, "nullable": false, "handle": "channelId" }, { "description": "The Markdown message body to post.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The Markdown message body to post." }, "nullable": false, "handle": "message" }, { "description": "Optional root post ID for replying in a thread.", "jsonSchema": { "type": "string", "minLength": 1, "description": "Optional root post ID for replying in a thread." }, "nullable": true, "handle": "rootId", "value": null }, { "description": "Optional Mattermost post props object.", "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "Optional Mattermost post props object." }, "nullable": true, "handle": "props", "value": null } ], "outputs": [ { "description": "A Mattermost post object.", "jsonSchema": { "type": "object", "properties": {}, "additionalProperties": true, "description": "A Mattermost post object." }, "nullable": false, "handle": "post" }, { "description": "The raw Mattermost API JSON response.", "jsonSchema": { "description": "The raw Mattermost API JSON response." }, "nullable": false, "handle": "raw" } ], "name": "Post to Mattermost", "executor": { "kind": "connector", "action": "mattermost.create_post" } } }, "n_0dfa46acfa724d38": { "inputs": { "ids": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_3e4ad16865cfc928", "output": "ids" } ] }, "hasResults": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_3e4ad16865cfc928", "output": "hasResults" } ] } }, "name": "Remember search results", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "string" } }, "nullable": false, "handle": "ids" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "hasResults" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "done" } ], "name": "Remember search results", "moduleId": "n_9aa1e3aa0cd6a0d7", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } } }, "edges": [ { "source": "n_0f3445d1e486b159", "target": "n_e50fd7cac2a37d8a" }, { "source": "n_e50fd7cac2a37d8a", "target": "n_3e4ad16865cfc928" }, { "source": "n_3e4ad16865cfc928", "target": "n_3da8fde9c7a9ed57" }, { "source": "n_3da8fde9c7a9ed57", "target": "n_b949b3de955881f8" }, { "source": "n_b949b3de955881f8", "target": "n_bd63947595e026f1", "sourceHandle": "send" }, { "source": "n_bd63947595e026f1", "target": "n_3da8fde9c7a9ed57" }, { "source": "n_b949b3de955881f8", "target": "n_0dfa46acfa724d38", "sourceHandle": "otherwise" } ] } }, "modules": { "n_66a6d12b7462473b": { "name": "Find new posts", "imports": [], "source": "export default async function plan({posts,includes},context){if(!Array.isArray(posts))throw Error('Missing X results');if(posts.length&&(!includes||!Array.isArray(includes.users)))throw Error('Missing expanded authors');const prev=await context.state.get('x-search-ids');if(prev!==null&&!Array.isArray(prev))throw Error('Invalid previous IDs');const ids=[],queue=[];for(const p of posts){const raw=p.raw??p,id=p.id,text=p.text,authorId=p.authorId??raw.author_id;if(typeof id!=='string'||!id||typeof text!=='string'||typeof authorId!=='string')throw Error('Incomplete X post');ids.push(id);const u=includes.users.find(x=>x.id===authorId);if(!u||typeof u.username!=='string'||!u.username||typeof u.name!=='string')throw Error('Missing X author');queue.push({id,text,username:u.username,name:u.name,photo:u.profile_image_url??''})}let pending=[];for(const x of queue){if(prev?.includes(x.id))break;pending.push(x)}return{pending,ids,hasResults:ids.length>0}}\n" }, "n_c5e0577f692f90fc": { "name": "Prepare next notification", "imports": [], "source": "export default function next({pending,previousState}){const i=previousState?.index??0;if(!Array.isArray(pending)||!Number.isSafeInteger(i)||i<0)throw Error('Invalid notification state');const p=pending[i];if(!p)return{more:false,state:{index:i},message:'-',props:{}};const link='https://x.com/'+encodeURIComponent(p.username)+'/status/'+encodeURIComponent(p.id),a={text:p.text,author_name:p.name+' ('+p.username+')',author_link:'https://x.com/'+encodeURIComponent(p.username)};if(p.photo)a.author_icon=p.photo;return{more:true,state:{index:i+1},message:link,props:{attachments:[a]}}}\n" }, "n_9aa1e3aa0cd6a0d7": { "name": "Remember search results", "imports": [], "source": "export default async function commit({ids,hasResults},context){if(hasResults)await context.state.set('x-search-ids',ids);return{done:true}}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }