Action

Append to Logseq

Posted by kerim, Last update 6 days ago

UPDATES

6 days ago

Changelog

[1.2.2] — 2026-05-22

Fixed

  • Drafts crash on send. Removed the intermediate “Sending to Logseq…” progress banner. In Drafts 52.0.1 / macOS 26.3.1 it could trip a dispatch_sync-on-owned-queue assertion when UserNotification scheduling raced with ActionKit teardown, killing the app. Progress messages now go to the script console only.
  • script.complete() never called — Operation timed out warning on drafts with many child lines. Total runtime is now well under the async-script threshold.

Changed

  • Single final banner. When the parent block landed but children failed, earlier versions could fire a warning and a success banner back-to-back — the same rapid-fire UserNotification pattern as above. Now exactly one final banner fires (success XOR warning).

Performance

  • Batched child inserts. Children are now inserted in a single logseq.Editor.insertBatchBlock call instead of one insertBlock per line. Total Logseq HTTP round-trips per send: 2, regardless of child count (was 1 + N). A draft with 10 child lines that previously took ~3 seconds now completes near-instantly.

Compatibility

  • No breaking changes. Same draft format, same target (today’s journal root), same API token / credential storage.
show all updates...

6 days ago

Changelog

[1.2.2] — 2026-05-22

Fixed

  • Drafts crash on send. Removed the intermediate “Sending to Logseq…” progress banner. In Drafts 52.0.1 / macOS 26.3.1 it could trip a dispatch_sync-on-owned-queue assertion when UserNotification scheduling raced with ActionKit teardown, killing the app. Progress messages now go to the script console only.
  • script.complete() never called — Operation timed out warning on drafts with many child lines. Total runtime is now well under the async-script threshold.

Changed

  • Single final banner. When the parent block landed but children failed, earlier versions could fire a warning and a success banner back-to-back — the same rapid-fire UserNotification pattern as above. Now exactly one final banner fires (success XOR warning).

Performance

  • Batched child inserts. Children are now inserted in a single logseq.Editor.insertBatchBlock call instead of one insertBlock per line. Total Logseq HTTP round-trips per send: 2, regardless of child count (was 1 + N). A draft with 10 child lines that previously took ~3 seconds now completes near-instantly.

Compatibility

  • No breaking changes. Same draft format, same target (today’s journal root), same API token / credential storage.

3 months ago

This version now checks if the API is running first, posting an error instead of crashing if it is not.

4 months ago

Updated formatting of the Readme

4 months ago

Updated the documentation to make it clear that this is for Logseq DB.

4 months ago

Updated to strip existing some formatting (as described) before import.

Send to Logseq Journal - Drafts Action

Appends draft content to today’s journal page in Logseq. The first line becomes the parent block, and remaining lines become nested children.

Prerequisites

  1. Logseq version 2.x.x (The DB version of Logseq.)

  2. Logseq HTTP API Server must be enabled:

    • Open Logseq → Settings → Features
    • Enable “HTTP APIs server”
    • Note your API token (or set one)
  3. Logseq must be running when you use the action

Setting Up the Action in Drafts

Step 1: Create New Action

  1. Open Drafts
  2. Go to Actions list
  3. Tap + to create a new action
  4. Configure:
    • Name: Send to Logseq Journal
    • Icon: Choose an icon (e.g., arrow.up.doc)
    • Color: Choose a color

Step 2: Add Script Step

  1. Tap Steps
  2. Tap +AdvancedScript
  3. Enable “Allow asynchronous execution” (critical!)
  4. Paste the contents of send-to-logseq-journal.js

Step 3: Configure After Success

  1. Go to action settings
  2. Under After Success:
    • Add Tag: sent-to-logseq
    • Archive: Enable

Step 4: First Run - API Token

On first run, Drafts will prompt for your Logseq API token:

  1. Get your token from Logseq → Settings → Features → HTTP APIs server
  2. Enter the token when prompted
  3. The token is stored securely in Drafts’ credential system

Usage

  1. Write your content in a draft
  2. First line = parent block
  3. Additional lines = child blocks (nested under parent)
  4. Run the action
  5. Content appears in today’s Logseq journal

Example

Draft content:

## Meeting notes from project sync
- Discussed timeline for Q1
- Action items assigned to team
- Follow-up scheduled for Friday

Result in Logseq journal:

- Meeting notes from project sync
  - Discussed timeline for Q1
  - Action items assigned to team
  - Follow-up scheduled for Friday

Content cleanup:

  • Header marks (#, ##, etc.) are stripped from the first line
  • Bullet prefixes (-, *, +) are stripped from child lines
  • Numbered lists (1., 2., etc.) are preserved as-is

Troubleshooting

“HTTP request failed”

  • Ensure Logseq is running
  • Verify HTTP API server is enabled in Logseq settings
  • Check that port 12315 is not blocked

“Failed to append block”

  • Verify your API token is correct
  • Try resetting credentials: long-press action → Manage Credentials → Forget

Wrong journal date format

The script uses Logseq’s default format: “Jan 17th, 2026”

If your Logseq uses a different journal format, edit the formatLogseqJournalDate() function in the script.

Resetting Credentials

To change your API token:

  1. Long-press the action
  2. Select Manage Credentials
  3. Tap Forget on “Logseq API”
  4. Run the action again to enter new token

Steps

  • script

    // Send to Logseq Journal - Drafts Action Script
    // Version: 1.2.2
    //
    // This script appends draft content to today's journal in Logseq.
    // First line becomes the parent block, remaining lines become children.
    //
    // Requirements:
    // - Logseq running with HTTP API server enabled
    // - API token configured in Logseq settings
    // - "Allow asynchronous execution" enabled in Drafts action step
    //
    // Note on banners: only one final app.displayXxxMessage() is ever fired.
    // Earlier versions used an intermediate displayInfoMessage() progress
    // banner; in Drafts 52.0.1 / macOS 26.3.1 rapid-fire UserNotification
    // scheduling raced with ActionKit teardown and crashed Drafts ("BUG IN
    // CLIENT OF LIBDISPATCH: dispatch_sync called on queue already owned by
    // current thread" on com.agiletortoise.Drafts5.scriptEngine). Progress
    // now goes to console.log only.
    //
    // Note on batching: all children are inserted in a single
    // logseq.Editor.insertBatchBlock call (total = 2 HTTP round-trips,
    // regardless of child count). Previously did 1 + N sequential round-trips,
    // which often tripped Drafts' async-script timeout warning on larger
    // drafts.
    
    // ============================================
    // Configuration
    // ============================================
    
    const LOGSEQ_API_URL = "http://127.0.0.1:12315/api";
    
    // ============================================
    // Helper Functions
    // ============================================
    
    /**
     * Strip markdown header marks from text (e.g., "## Title" -> "Title")
     */
    function stripHeaderMarks(text) {
        return text.replace(/^#+\s*/, "");
    }
    
    /**
     * Strip list marker prefixes from text (e.g., "- item" -> "item")
     * Handles: -, *, + (but NOT numbered lists, which are preserved)
     */
    function stripListPrefix(text) {
        // Strip leading whitespace, then bullet prefix (-, *, +) followed by space
        return text.replace(/^\s*[\-\*\+]\s+/, "");
    }
    
    /**
     * Format date for Logseq journal page name
     * Logseq default format: "Jan 17th, 2026"
     */
    function formatLogseqJournalDate(date) {
        const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
                        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
        const day = date.getDate();
        const suffix = (day === 1 || day === 21 || day === 31) ? "st" :
                       (day === 2 || day === 22) ? "nd" :
                       (day === 3 || day === 23) ? "rd" : "th";
        return months[date.getMonth()] + " " + day + suffix + ", " + date.getFullYear();
    }
    
    /**
     * Make a request to Logseq HTTP API
     */
    function logseqRequest(method, args, token) {
        var http = HTTP.create();
    
        try {
            var response = http.request({
                "url": LOGSEQ_API_URL,
                "method": "POST",
                "encoding": "json",
                "headers": {
                    "Authorization": "Bearer " + token,
                    "Content-Type": "application/json"
                },
                "data": {
                    "method": method,
                    "args": args
                }
            });
            return response;
        } catch (e) {
            return {
                success: false,
                statusCode: 0,
                responseText: ""
            };
        }
    }
    
    /**
     * Parse response from Logseq API
     */
    function parseResponse(response) {
        if (!response.success) {
            return {
                success: false,
                error: "HTTP request failed with status " + response.statusCode
            };
        }
    
        try {
            var data = JSON.parse(response.responseText);
            if (data.error) {
                return {
                    success: false,
                    error: data.error.message || JSON.stringify(data.error)
                };
            }
            return {
                success: true,
                result: data
            };
        } catch (e) {
            return {
                success: false,
                error: "Failed to parse response: " + e.message
            };
        }
    }
    
    // ============================================
    // Main Script
    // ============================================
    
    function main() {
        // 1. Get API token from secure credential storage
        var credential = Credential.create("Logseq API", "Logseq HTTP API authentication token");
        credential.addPasswordField("token", "API Token");
    
        if (!credential.authorize()) {
            app.displayErrorMessage("Authorization cancelled");
            context.fail();
            return;
        }
    
        var token = credential.getValue("token");
    
        if (!token || token.trim() === "") {
            app.displayErrorMessage("No API token provided");
            context.fail();
            return;
        }
    
        // 2. Parse draft content
        var lines = draft.content.split("\n").filter(function(line) {
            return line.trim() !== "";
        });
    
        if (lines.length === 0) {
            app.displayErrorMessage("Draft is empty");
            context.fail();
            return;
        }
    
        var parentContent = stripHeaderMarks(lines[0]);
        var childLines = lines.slice(1).map(function(line) {
            return stripListPrefix(line);
        });
    
        // 3. Get today's journal page name
        var today = new Date();
        var journalPage = formatLogseqJournalDate(today);
    
        // 4. Append parent block to journal page.
        // No app.displayInfoMessage() here — see header note on the ActionKit
        // dispatch_sync crash; intermediate banners are intentionally suppressed.
        console.log("appendBlockInPage: " + journalPage);
    
        var appendResponse = logseqRequest(
            "logseq.Editor.appendBlockInPage",
            [journalPage, parentContent],
            token
        );
    
        var appendResult = parseResponse(appendResponse);
    
        if (!appendResult.success) {
            app.displayErrorMessage("Failed to append block: " + appendResult.error);
            context.fail();
            return;
        }
    
        // 5. Insert all children in ONE batch call under the parent block.
        // insertBatchBlock with sibling:false makes the array items children
        // of parentUUID. Single round-trip regardless of child count.
        var parentBlock = appendResult.result;
    
        if (childLines.length > 0 && parentBlock && parentBlock.uuid) {
            var parentUUID = parentBlock.uuid;
            var childBlocks = childLines.map(function(line) {
                return { "content": line };
            });
    
            console.log("insertBatchBlock under parent UUID: " + parentUUID + " (children: " + childLines.length + ")");
    
            var batchResponse = logseqRequest(
                "logseq.Editor.insertBatchBlock",
                [parentUUID, childBlocks, {"sibling": false}],
                token
            );
    
            var batchResult = parseResponse(batchResponse);
    
            if (!batchResult.success) {
                // Single final banner (see header note on the ActionKit dispatch_sync
                // crash; never fire two banners back-to-back). Parent already landed.
                app.displayWarningMessage("Sent to " + journalPage + " — children failed: " + batchResult.error);
                return;
            }
        }
    
        // 6. Success!
        var childCount = childLines.length;
        var message = "Sent to " + journalPage;
        if (childCount > 0) {
            message += " (" + childCount + " child block" + (childCount > 1 ? "s" : "") + ")";
        }
        app.displaySuccessMessage(message);
    }
    
    main();
    script.complete();
    

Options

  • After Success Archive , Tags: sent-to-logseq
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.