{ "kind": "open-flow-workflow", "version": 1, "name": "Glasp Highlight Export", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_65bd68698bb93645": { "name": "Every six hours", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "0 */6 * * *", "timezone": "UTC" } ] }, "n_d83e991372b0c1cf": { "inputs": { "stateKey": { "kind": "value", "value": "glasp:export-tracking" } }, "name": "Prepare Glasp checkpoint", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "stateKey" } ], "outputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "updatedAfter" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "startedAt" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "exportedDocs" } ], "name": "Prepare Glasp checkpoint", "moduleId": "n_742e364218bd4692", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_c7b9b4a4ce1dceba": { "inputs": { "updatedAfter": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d83e991372b0c1cf", "output": "updatedAfter" } ] }, "accessToken": { "kind": "unset" } }, "name": "Collect Glasp pages", "timeoutMs": 3000000, "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "updatedAfter" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "accessToken" } ], "outputs": [ { "jsonSchema": { "type": "array" }, "nullable": false, "handle": "pages" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "pageCount" } ], "name": "Collect Glasp pages", "moduleId": "n_2faabea11634d8d6", "capabilities": [ { "kind": "http", "hosts": [ "api.glasp.co" ] } ] } }, "n_7afabbce1a44f61f": { "inputs": { "pages": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c7b9b4a4ce1dceba", "output": "pages" } ] }, "exportedDocs": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d83e991372b0c1cf", "output": "exportedDocs" } ] }, "startedAt": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_d83e991372b0c1cf", "output": "startedAt" } ] } }, "name": "Format new highlight documents", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array" }, "nullable": false, "handle": "pages" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "exportedDocs" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "startedAt" } ], "outputs": [ { "jsonSchema": { "type": "array" }, "nullable": false, "handle": "documents" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "message" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "tracking" } ], "name": "Format new highlight documents", "moduleId": "n_f14bfaef4fdc0684", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_b765b424ff429a13": { "inputs": { "stateKey": { "kind": "value", "value": "glasp:export-tracking" }, "tracking": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7afabbce1a44f61f", "output": "tracking" } ] }, "documents": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7afabbce1a44f61f", "output": "documents" } ] }, "count": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7afabbce1a44f61f", "output": "count" } ] }, "message": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_7afabbce1a44f61f", "output": "message" } ] } }, "name": "Save export tracking", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "stateKey" }, { "jsonSchema": { "type": "object" }, "nullable": false, "handle": "tracking" }, { "jsonSchema": { "type": "array" }, "nullable": false, "handle": "documents" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "message" } ], "outputs": [ { "jsonSchema": { "type": "array" }, "nullable": false, "handle": "documents" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "message" } ], "name": "Save export tracking", "moduleId": "n_97bf666924a6de0d", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_8ac6865df9004480": { "name": "Formatted highlight export", "kind": "outputs", "outputDefs": [ { "jsonSchema": { "type": "array" }, "nullable": false, "handle": "documents" }, { "jsonSchema": { "type": "integer" }, "nullable": false, "handle": "count" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "message" } ], "outputMappings": { "documents": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_b765b424ff429a13", "output": "documents" } ] }, "count": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_b765b424ff429a13", "output": "count" } ] }, "message": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_b765b424ff429a13", "output": "message" } ] } } } }, "edges": [ { "source": "n_65bd68698bb93645", "target": "n_d83e991372b0c1cf" }, { "source": "n_d83e991372b0c1cf", "target": "n_c7b9b4a4ce1dceba" }, { "source": "n_c7b9b4a4ce1dceba", "target": "n_7afabbce1a44f61f" }, { "source": "n_7afabbce1a44f61f", "target": "n_b765b424ff429a13" }, { "source": "n_b765b424ff429a13", "target": "n_8ac6865df9004480" } ] } }, "modules": { "n_742e364218bd4692": { "name": "Prepare Glasp checkpoint", "imports": [], "source": "export function prepareState({snapshot,startedAt}) {\n const state=snapshot??{};\n if(!state||typeof state!=='object'||Array.isArray(state))throw new Error('Glasp tracking state must be an object.');\n const now=Date.parse(startedAt);\n if(!Number.isFinite(now))throw new Error('Invalid Glasp run start time.');\n const exportedDocs=state.exportedDocs??{};\n if(!exportedDocs||typeof exportedDocs!=='object'||Array.isArray(exportedDocs))throw new Error('Glasp exportedDocs must be an object.');\n for(const date of Object.values(exportedDocs))if(typeof date!=='string'||!Number.isFinite(Date.parse(date)))throw new Error('Glasp tracking dates must be valid timestamps.');\n const base=state.lastRunAt?Date.parse(state.lastRunAt):now-24*60*60*1000;\n if(!Number.isFinite(base))throw new Error('Invalid saved Glasp checkpoint.');\n return {updatedAfter:new Date(base-5*60*1000).toISOString(),startedAt,exportedDocs:{...exportedDocs}};\n}\n\nexport default async function prepareGlasp({stateKey},context) {\n return prepareState({snapshot:await context.state.get(stateKey),startedAt:new Date().toISOString()});\n}\n" }, "n_2faabea11634d8d6": { "name": "Collect Glasp pages", "imports": [], "source": "export default async function collectGlaspPages({updatedAfter,accessToken},context) {\n if(typeof accessToken!=='string'||!accessToken.trim()||/[\\r\\n]/.test(accessToken))throw new Error('Configure a Glasp access token through a secret Variable.');\n if(typeof updatedAfter!=='string'||!Number.isFinite(Date.parse(updatedAfter)))throw new Error('updatedAfter must be a valid timestamp.');\n const pages=[],seen=new Set();\n let cursor=null;\n const deadline=Date.now()+3000000;\n for(let page=0;page<100;page++){\n context.signal?.throwIfAborted();\n const remaining=deadline-Date.now();\n if(remaining<=0)throw new Error('Glasp pagination exceeded its time limit.');\n const url=new URL('https://api.glasp.co/v1/highlights/export');\n url.searchParams.set('updatedAfter',updatedAfter);\n if(cursor!==null)url.searchParams.set('pageCursor',cursor);\n const timeout=AbortSignal.timeout(Math.min(30000,remaining));\n const signal=context.signal?AbortSignal.any([context.signal,timeout]):timeout;\n const response=await context.fetch(url,{method:'GET',headers:{Authorization:'Bearer '+accessToken},signal});\n if(!response.ok)throw new Error('Glasp export failed with HTTP '+response.status+'.');\n const body=await response.json();\n if(!body||typeof body!=='object'||Array.isArray(body)||!Array.isArray(body.results))throw new Error('Glasp export did not return a results array.');\n const next=body.nextPageCursor??null;\n if(next!==null&&typeof next!=='string')throw new Error('Glasp cursor must be text or null.');\n pages.push(body);\n if(!next)return {pages,pageCount:pages.length};\n if(seen.has(next))throw new Error('Glasp repeated a pagination cursor.');\n seen.add(next);cursor=next;\n }\n throw new Error('Glasp has more than 100 pages; tracking has not advanced.');\n}\n" }, "n_f14bfaef4fdc0684": { "name": "Format new highlight documents", "imports": [], "source": "export function formatPages({pages,exportedDocs,startedAt,completedAt}) {\n if(!Array.isArray(pages)||!pages.length||pages.length>100)throw new Error('Expected 1–100 Glasp pages.');\n const results=[];\n for(const page of pages){\n if(!page||typeof page!=='object'||!Array.isArray(page.results))throw new Error('Glasp page is missing results.');\n if(page.nextPageCursor!=null&&typeof page.nextPageCursor!=='string')throw new Error('Invalid Glasp cursor.');\n results.push(...page.results);\n }\n if(pages.at(-1).nextPageCursor)throw new Error('Glasp pagination is incomplete; tracking has not advanced.');\n const text=(value,name,fallback='')=>{if(value==null)return fallback;if(typeof value!=='string')throw new Error('Glasp '+name+' must be text.');return value;};\n const fresh=[];\n for(const doc of results){\n if(!doc||typeof doc!=='object'||Array.isArray(doc)||typeof doc.id!=='string'||!doc.id)throw new Error('Glasp document ID must be nonempty text.');\n if(!Array.isArray(doc.highlights))throw new Error('Glasp highlights must be an array.');\n if(!Object.hasOwn(exportedDocs,doc.id)&&doc.highlights.length)fresh.push(doc);\n }\n const tracked=Object.fromEntries(Object.entries(exportedDocs));\n const documents=fresh.map(doc=>{\n const title=text(doc.title,'title')||'Untitled';\n const url=text(doc.url,'url'),glasp_url=text(doc.glasp_url,'glasp_url');\n const tags=doc.tags??[];\n if(!Array.isArray(tags)||tags.some(tag=>typeof tag!=='string'))throw new Error('Glasp tags must be an array of strings.');\n const document_note=text(doc.document_note,'document_note');\n const lines=['## '+title,'','**URL:** '+url,'**Glasp:** '+glasp_url];\n if(tags.length)lines.push('**Tags:** '+tags.join(', '));\n if(document_note)lines.push('**Note:** '+document_note);\n lines.push('','### Highlights','');\n const highlights=doc.highlights.map(highlight=>{\n if(!highlight||typeof highlight!=='object'||Array.isArray(highlight))throw new Error('Glasp highlight must be an object.');\n const timestamp=text(highlight.highlighted_at,'highlighted_at');\n if(!Number.isFinite(Date.parse(timestamp)))throw new Error('Glasp highlight time must be valid.');\n const result={id:text(highlight.id,'highlight ID'),text:text(highlight.text,'highlight text'),note:text(highlight.note,'highlight note'),color:text(highlight.color,'highlight color'),highlighted_at:timestamp};\n lines.push(result.text.split('\\n').map(line=>'> '+line).join('\\n'));\n if(result.note)lines.push('','**Note:** '+result.note);\n lines.push('','- *'+result.color+' highlight, '+new Date(timestamp).toISOString().slice(0,10)+'*','');\n return result;\n });\n Object.defineProperty(tracked,doc.id,{value:completedAt,enumerable:true,writable:true,configurable:true});\n return {documentId:doc.id,title,url,glasp_url,domain:text(doc.domain,'domain'),category:text(doc.category,'category'),tags,author:text(doc.author,'author'),thumbnail_url:text(doc.thumbnail_url,'thumbnail_url'),document_note,is_favorite:doc.is_favorite??false,highlightCount:highlights.length,highlightsText:highlights.map(highlight=>highlight.text+(highlight.note?' [Note: '+highlight.note+']':'')).join('\\n\\n'),highlightsMarkdown:lines.join('\\n'),highlights,createdAt:text(doc.created_at,'created_at'),updatedAt:text(doc.updated_at,'updated_at')};\n });\n const oldest=Date.parse(completedAt)-30*24*60*60*1000;\n for(const[id,exportedAt]of Object.entries(tracked))if(Date.parse(exportedAt)