Action
Send Draft to Notesnook Inbox
Posted by stevesbrain,
Last update
24 days ago
UPDATES
24 days ago
Fixed up long body paragraphs becoming truncated titles
24 days ago
Fixed up long body paragraphs becoming truncated titles
24 days ago
Moved all metadata to the end
27 days ago
Include metadata in header and footer
27 days ago
Fixed uncommon bullets not being translated into HTML correctly
27 days ago
Converting Markdown to HTML to properly keep formatting
Used to share Drafts items to Notesnook Inbox API. Can be run on batch items.
Steps
-
script
// ============================================================================ // Send Draft to Notesnook Inbox // ---------------------------------------------------------------------------- // Posts the current draft to your Notesnook account via the Inbox API. // Notesnook Inbox API docs: https://notesnook.com/help/inbox-api/getting-started // Drafts scripting reference: https://scripting.getdrafts.com // // HOW TO INSTALL // 1. In Drafts, create a new Action (or edit an existing one) and add a // single "Script" action step. // 2. Paste this entire file into that Script step. // 3. In Notesnook: Settings > Account > Inbox > enable "Inbox API", then // create an API key under "API Keys" (give it a name/expiry). // 4. Run the action on any draft. The first run will prompt you to paste // the API key — it is stored securely in Drafts' Credentials manager // (Settings > Credentials), NOT in this script, and reused after that. // To change/reset it later, delete the "Notesnook Inbox API" entry in // Settings > Credentials and run the action again. // 5. Optionally fill in TAG_ID and/or NOTEBOOK_ID below. // // HOW CONTENT IS MAPPED // - Drafts treats the first line of a draft as its "title" and everything // after it as the "body" ([[title]] / [[body]] template tags). // - If a title line exists (i.e. there is body text after it) AND that // first line is no longer than MAX_TITLE_LENGTH, the title is sent as // the note's title and the remaining text is sent as the body. // - Otherwise — there's no separate title, or the "first line" is really // just a long, un-wrapped paragraph rather than an actual title — the // entire draft content is sent as the body (so nothing is silently // swallowed into an oversized title/truncated by Notesnook's display), // and a generic fallback title is used — "Drafts DD-MMM-YYYY HH:MM:SS" // (the current date/time) — since the Inbox API requires a non-empty // title field. // - The body is run through Drafts' GitHub Markdown renderer before being // sent, so Markdown syntax (headings, **bold**, lists, links, tables, // etc.) is converted to real HTML rather than sent as literal text. The // title itself is sent as plain text, with leading "#" markers/stray // whitespace stripped (the API's title field is a plain string, not // HTML, and shouldn't show raw Markdown syntax). // - Ordinary multi-line text is split into one paragraph per line rather // than joined with <br>, because Notesnook's sanitizer strips <br> but // keeps <p> paragraphs — lists, tables, blockquotes and code blocks are // left as real Markdown structures instead. // - A "Open original draft in Drafts" deep link (draft.permalink, a // drafts://open?uuid=... URL) is appended to the end of the note body, // as plain visible text rather than a clickable link — Notesnook's own // sanitizer only keeps http/https hrefs, so a drafts:// <a href> would // arrive stripped of its link. Copy/paste the text into Safari's // address bar (or Spotlight on Mac) to jump back into Drafts. // - A blockquote with the draft's created date, last modified date, and // tags is appended at the very bottom of the note, after the deep link. // ============================================================================ // ---- CONFIGURATION --------------------------------------------------------- // Inbox API URL. Only change this if you are self-hosting the Inbox API. // https://notesnook.com/help/inbox-api/self-hosting-inbox-api const API_URL = "https://inbox.notesnook.com/"; // Optional: assign a tag to every note sent from this action. // In Notesnook, right-click a tag and choose "Copy ID" to get this value. const TAG_ID = ""; // e.g. "67aecf3b9e1398484554bc90" // Optional: file every note sent from this action into a notebook. // In Notesnook, right-click a notebook and choose "Copy ID" to get this value. const NOTEBOOK_ID = ""; // e.g. "67aecf3b9e1398484554bc90" // A draft's first line is only treated as a real title if it's no longer // than this many characters. Longer first lines are usually just an // un-wrapped paragraph rather than an intentional title, and using them // as the title would bury/truncate that text instead of showing it in // the body. const MAX_TITLE_LENGTH = 100; // ----------------------------------------------------------------------------- // Prompts once for the Inbox API key and stores it in Drafts Credentials. // Subsequent runs reuse the stored value without prompting. function getApiKey() { let credential = Credential.create( "Notesnook Inbox API", "Notesnook Inbox API key (Notesnook Settings > Account > Inbox > API Keys)." ); credential.addPasswordField("apiKey", "Inbox API Key"); credential.authorize(); return credential.getValue("apiKey"); } // Generic fallback title: "Drafts DD-MMM-YYYY HH:MM:SS", e.g. "Drafts 17-Aug-2026 14:32:05". function getFallbackTitle() { return "Drafts " + draft.processTemplate("[[date|%d-%b-%Y %H:%M:%S]]"); } // Bullet lines pasted in from iOS (e.g. copied out of Apple Notes) often // look like "•\tSome text" — a bullet glyph, a tab, and an invisible // word-joiner character (U+2060) — rather than real Markdown list syntax // ("- ", "* ", "+ "). cmark doesn't recognize those as a list, so it // treats them as plain paragraph lines. This rewrites any line starting // with a bullet-ish glyph into a proper Markdown list item so the // renderer produces a real <ul>/<li> list. function normalizeBulletLines(text) { let bulletPattern = new RegExp("^([ \\t]*)[\u2022\u25E6\u25AA\u2023][\\t \u2060]*(.*)$"); return text .split("\n") .map(function (line) { let match = line.match(bulletPattern); return match ? match[1] + "- " + match[2] : line; }) .join("\n"); } function isListItemLine(line) { return /^[ \t]*([-*+]|\d+[.)])[ \t]+/.test(line); } function isBlockquoteLine(line) { return /^[ \t]*>/.test(line); } function isTableLine(line) { return ( /^[ \t]*\|/.test(line) || /^[ \t]*:?-{2,}:?[ \t]*(\|[ \t]*:?-{2,}:?[ \t]*)+\|?[ \t]*$/.test(line) ); } function isFenceLine(line) { return /^[ \t]*(```|~~~)/.test(line); } // Notesnook's sanitizer keeps <p> paragraphs, but <br> is not in its // allow-list of surviving tags, so it gets stripped on arrival and lines // that were only separated by a <br> silently merge back together. cmark // paragraphs (blank-line separated) do survive, so this turns ordinary // multi-line text into one paragraph per line instead of relying on // <br>. List items, tables, blockquotes and fenced code blocks are left // untouched, so real Markdown structures — including the bullet lists // normalizeBulletLines() just created — still render as a single grouped // list/table/etc. rather than one paragraph per line. function expandPlainTextLineBreaks(markdownText) { return markdownText .split(/\n{2,}/) .map(function (block) { let lines = block.split("\n"); if (lines.length <= 1) { return block; } let isStructured = lines.some(isFenceLine) || lines.every(function (line) { return ( line.trim().length === 0 || isListItemLine(line) || isBlockquoteLine(line) || isTableLine(line) ); }); return isStructured ? block : lines.join("\n\n"); }) .join("\n\n"); } // Converts the draft's Markdown into HTML using Drafts' built-in GitHub // Markdown (cmark-gfm) renderer, so headings, bold/italic, lists, links, // tables, etc. all come through as real HTML rather than literal text. // cmark's output is always well-formed HTML, so it also satisfies the // Inbox API's tag-balance check (an unclosed/mismatched tag anywhere in // the payload causes the whole note to be dumped as a raw code block). function markdownToHtml(markdownText) { let md = GitHubMarkdown.create(); md.tables = true; md.strikethrough = true; md.taskLists = true; // hardBreaks intentionally left at its default (false): it renders // soft line breaks as <br>, but Notesnook's sanitizer strips <br> // outright, so that tag never survives. expandPlainTextLineBreaks() // handles line breaks instead, via real paragraphs. // Keep default "safe" (true): strips raw HTML/unsafe links from the // source, which is a sane default and mirrors Notesnook's own sanitizer. let normalized = expandPlainTextLineBreaks(normalizeBulletLines(markdownText)); return md.render(normalized); } function htmlEscape(text) { return text .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """); } // A small HTML block with the deep link back to this exact draft, appended // after the rendered body. draft.permalink is a drafts://open?uuid=... // URL — Drafts only exposes a custom-scheme link, no http(s) equivalent. // Notesnook's own sanitizer only preserves links with an http/https // address (see "What HTML can I send?" in the Inbox API docs), so an // <a href="drafts://..."> arrives with its href stripped — a link // element with nothing to click. Rendering it as plain visible text // instead survives sanitization intact: it can be copied and pasted into // Safari's address bar (or Spotlight on Mac) to jump back into Drafts. function buildDeepLinkHtml() { return ( "<p>Open original draft in Drafts: <code>" + htmlEscape(draft.permalink) + "</code></p>" ); } // A blockquote of draft metadata (created date, modified date, tags), // inserted at the very top of the note. Dates use the same // "DD-MMM-YYYY HH:MM:SS" format as the fallback title, via the built-in // [[created]]/[[modified]] template tags. function buildMetadataBlockquoteHtml() { let created = draft.processTemplate("[[created|%d-%b-%Y %H:%M:%S]]"); let modified = draft.processTemplate("[[modified|%d-%b-%Y %H:%M:%S]]"); let tags = draft.tags.length > 0 ? draft.tags.join(", ") : "None"; return ( "<blockquote>" + "<p>Created: " + htmlEscape(created) + "</p>" + "<p>Modified: " + htmlEscape(modified) + "</p>" + "<p>Tags: " + htmlEscape(tags) + "</p>" + "</blockquote>" ); } function main() { if (draft.content.trim().length === 0) { context.fail("Draft is empty — nothing to send."); return; } let apiKey = getApiKey(); if (!apiKey) { context.fail("Notesnook Inbox API key is required."); return; } // [[display_title]] = first line of the draft, cleaned up as it would // display in the draft list — leading "#" heading markers and stray // whitespace stripped, since the API's title field is plain text and // shouldn't show raw Markdown syntax. [[trimmed_body]] = everything // after the first line, trimmed — empty if the draft is a single line. let title = draft.processTemplate("[[display_title]]").trim(); let body = draft.processTemplate("[[trimmed_body]]"); let noteTitle; let noteBody; let hasRealTitle = body.length > 0 && title.length > 0 && title.length <= MAX_TITLE_LENGTH; if (hasRealTitle) { // A short, genuine title line exists: use it as the title, rest of // the draft as body. noteTitle = title; noteBody = body; } else { // Either there's no separate title (single-line/single-block draft), // or the "first line" is too long to be a real title (just an // un-wrapped paragraph). Either way, the whole draft becomes the // body — so no text gets silently dropped into an oversized, // truncated title — and we use the generated fallback title. noteTitle = getFallbackTitle(); noteBody = draft.content; } let requestBody = { title: noteTitle, type: "note", source: "drafts-app", version: 1, content: { type: "html", data: markdownToHtml(noteBody) + buildDeepLinkHtml() + buildMetadataBlockquoteHtml() } }; if (TAG_ID) { requestBody.tagIds = [TAG_ID]; } if (NOTEBOOK_ID) { requestBody.notebookIds = [NOTEBOOK_ID]; } let http = HTTP.create(); let response = http.request({ url: API_URL, method: "POST", data: requestBody, encoding: "json", headers: { "Content-Type": "application/json", "Authorization": apiKey } }); if (response.success) { app.displaySuccessMessage("Sent to Notesnook Inbox."); } else { let detail = response.responseText || response.error || ("HTTP " + response.statusCode); console.log("Notesnook Inbox error: " + detail); context.fail("Notesnook Inbox error: " + detail); } } main();
Options
-
After Success Default Notification Info Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.