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

show all updates...

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 "•\t⁠Some 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, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;");
    }
    
    // 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.