Action
Send Draft to Notesnook Inbox
Posted by stevesbrain,
Last update
3 days ago
UPDATES
3 days ago
Fixed up long body paragraphs becoming truncated titles
3 days ago
Fixed up long body paragraphs becoming truncated titles
4 days ago
Moved all metadata to the end
6 days ago
Include metadata in header and footer
6 days ago
Fixed uncommon bullets not being translated into HTML correctly
6 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.