A technical reference for developers building local macOS integrations with Traverse. For workflow introductions and Shortcuts, see Automating Traverse with Shortcuts and URLs.

Document discovery requires Traverse 1.4.2 or later. URL automation does not require Traverse Pro; the Shortcuts actions do. This is a local URL-scheme API, not an HTTP service.

Endpoints at a glance

EndpointPurposeResult
traverse://openOpen an outline or nodeNavigation; no callback result
createOutlineCreate a new library outlineWrite-result callback
appendAppend parsed outline contentWrite-result callback
addNodeAdd a node or parse content, with optional attributesWrite-result callback
getCurrentNodeRead Traverse's current internal nodeNode callback; x-success required
getNodeRead a specified nodeNode callback
getNodeAttributesRead normalized attributesJSON in an attributes callback parameter
updateNodeUpdate text, note or attributesNode callback
listDocumentsDiscover library outlinesJSON on a private named pasteboard

Except for traverse://open, endpoints use traverse://x-callback-url/ENDPOINT. Endpoint names and query parameter names are case-sensitive. Find Node and Find Nodes are Shortcuts actions; there are currently no corresponding URL endpoints.

Encoding and target selection

Percent-encode query values, including text, JSON and nested callback URLs. Use a URL builder rather than string concatenation. For example, this builds a URL for adding an unchecked task; replace the document UUID before opening it:

import Foundation

var components = URLComponents()
components.scheme = "traverse"
components.host = "x-callback-url"
components.path = "/addNode"
components.queryItems = [
    URLQueryItem(name: "document", value: "DOCUMENT-UUID"),
    URLQueryItem(name: "text", value: "Research A & B"),
    URLQueryItem(name: "attributes", value: "task: open")
]
let captureURL = components.url!

For existing-document x-callback endpoints, Traverse tries to resolve document as a document UUID, then file as a file URL, then the selected library outline. This fallback also applies if a supplied UUID cannot be resolved. Use current references from document discovery or Traverse's Copy Document ID/Copy Link commands, and avoid relying on selection for destination-specific workflows.

For node operations, node accepts a UUID or a Traverse node link; nodeLink and url are also accepted as node-reference parameters. These extract the node ID only: supply the document target separately. Parent parameters on append/add require raw node UUIDs. A document reference or returned file URL does not grant direct filesystem access.

Opening an outline or node

traverse://open?document=DOCUMENT-UUID
traverse://open?document=DOCUMENT-UUID&node=NODE-UUID

Use document and node UUIDs copied from Traverse or returned by automation. This route navigates in Traverse; it does not return a success payload. The stable document UUID survives file moves and renames within the library.

Creating and appending content

createOutline

traverse://x-callback-url/createOutline?title=Project%20Plan&content=First%20idea&format=plainText

Requires a title parameter. Supply content, or its alias text, and optional format. Creates a new document in the selected library folder, falling back to the main library root. Returns a write-result callback.

append

traverse://x-callback-url/append?document=DOCUMENT-UUID&content=CONTENT&format=markdown

Supply content (or text) and a document target. Without node, parsed nodes are appended after the document's top-level nodes. Supply node=PARENT-NODE-UUID to append beneath that parent. Append preserves parsed hierarchy but does not apply separate note/attribute parameters to the first appended node.

addNode

traverse://x-callback-url/addNode?document=DOCUMENT-UUID&text=TEXT&attributes=task%3A%20open

Supply non-empty text, optional note and attributes, and a document target. Default mode=singleNode creates one node. Use parent=PARENT-NODE-UUID (or the alias node) to insert beneath an existing node; otherwise it is appended at the top level. If both are supplied, a valid parent takes precedence.

Use mode=parseOutline with a content format to turn text into multiple nodes. Separate note and attribute payloads apply to the first inserted node only. All three write endpoints return document details and the first new node when available.

Formats

  • plainText: indented plain text.
  • markdown: Markdown headings, lists and other supported structure.
  • opml: OPML outline content.

Missing or unrecognised format values use plainText. Missing or unrecognised add-node modes use singleNode. Format is used for parsed content; singleNode does not split text into multiple nodes. Malformed imported content may fall back to a simple text node, so a successful write is not a guarantee of a particular parsed hierarchy.

Reading and updating nodes

getCurrentNode

traverse://x-callback-url/getCurrentNode?x-success=ENCODED-CALLBACK-URL

Requires a valid x-success callback and a currently selected internal Traverse node. The callback includes title, text, note and url, plus node, document and file when available. This depends on the current UI selection; it is not a destination-picker operation.

getNode

traverse://x-callback-url/getNode?document=DOCUMENT-UUID&node=NODE-UUID&x-success=ENCODED-CALLBACK-URL

Returns title, text, note, document, file, node and url callback parameters. Always provide x-success to consume the result; without it, Traverse opens the node when possible.

getNodeAttributes

traverse://x-callback-url/getNodeAttributes?document=DOCUMENT-UUID&node=NODE-UUID&x-success=ENCODED-CALLBACK-URL

Returns one callback parameter named attributes, containing a JSON object of normalized attribute names and string values. Decode the URL query first, then parse its JSON value. For example, task values are returned as strings "true" or "false". Supply x-success; this endpoint produces no consumable result without it.

updateNode

traverse://x-callback-url/updateNode?document=DOCUMENT-UUID&node=NODE-UUID&note=NOTE&appendNote=true

Supply any of text, note and attributes. Omitted text/note fields remain unchanged. Note replacement is the default; appendNote=true appends it. Boolean query values accept true/yes/1/on or false/no/0/off. Returns the same node-result callback fields as getNode.

Attribute payloads

The addNode and updateNode endpoints accept a flat JSON object or line-based key/value text. Build and encode the whole payload as one query value:

{"task":"open","mark":"blue","url":"https://example.com"}
task: done
mark: green
status: Waiting

Names match an existing attribute key or unambiguous visible label. The destination document must define the intended attribute. Unknown, ambiguous or invalid entries are ignored; use getNodeAttributes if you need to verify that a requested value was applied. JSON scalar strings, numbers and booleans are accepted; do not send nested objects or arrays as attribute values.

Task values such as open, todo and false create an unchecked task; done, checked and true mark it complete. Marks accept colour names or 0–6. Custom attributes and structural Type/Fountain values are validated against the destination definitions. See the workflow article for the full friendly-value list.

Callbacks and operation completion

For the established x-callback endpoints, provide your own registered x-success and x-error receiver URLs. They must be percent-encoded as query values. Traverse appends result fields to the callback URL's query. Callback values are strings; decode nested JSON fields after parsing the URL.

  • Write-result callbacks for createOutline, append and addNode include title, document and file, with node/url when a new node exists.
  • Node-result callbacks for getNode and updateNode also include text and note.
  • getNodeAttributes returns the attributes JSON query value.
  • Error callbacks receive errorCode and errorMessage. Without x-error, failures beep.
  • Without x-success, create/append/add/getNode/updateNode open their resulting node when possible; getCurrentNode requires a callback and getNodeAttributes has no output without one.

Opening a URL successfully acknowledges app delivery, not a completed save. Use the operation callback to track completion. listDocuments is the exception: it uses the private pasteboard protocol below rather than x-success query parameters.

Error codeSituation
missing-titlecreateOutline has no title parameter
missing-documentNo document target can be resolved, including selection fallback
missing-nodeNo node reference can be decoded
missing-success / invalid-successRequired callback is absent, or a callback URL cannot be built
no-current-nodeNo internal node is selected for getCurrentNode
create-failed, append-failed, add-node-failedThe corresponding write failed
get-node-failed, get-attributes-failed, update-node-failedThe corresponding node operation failed

Use the error message for human-readable detail; do not parse messages as machine identifiers. Use a bounded timeout when waiting for app callbacks, because an unavailable app or callback receiver cannot complete the round trip.

Document discovery: listDocuments

Available in Traverse for macOS 1.4.2 and later. This local macOS protocol does not require Traverse Pro. It is not an HTTP service.

Request and response channel

Generate a fresh UUID for each request and open:

traverse://x-callback-url/listDocuments?request=REQUEST-UUID&version=2

request is required. Use the canonical uppercase UUID form (UUID.uuidString) to construct the response pasteboard name; Traverse normalises the request UUID to that form. version is optional (defaults to the current protocol); clients should send it explicitly. An unsupported supplied version produces a structured error.

Before opening the URL, obtain the named pasteboard:

com.habitualshaker.traverse.documents.REQUEST-UUID

Poll its data using this custom type:

com.habitualshaker.traverse.document-list

Traverse refreshes library metadata and publishes one UTF-8 JSON response. Poll asynchronously (for example, every 100 ms), with a bounded timeout (the example uses 10 seconds). Validate version and request identity before using the response. Release the named pasteboard with releaseGlobally() after consuming it or timing out. Independent requests have independent pasteboards; the general clipboard is never used.

The URL may launch Traverse. A native client can use NSWorkspace.OpenConfiguration.activates = false to request background delivery. Request a fresh list before showing destinations. Existing create/add/append/update URLs still perform all writes through Traverse.

Success response

{
  "version": 2,
  "requestID": "2BD4614C-240D-40A5-B69F-A30730D3AC54",
  "status": "success",
  "documents": [
    {
      "documentID": "9B44AD57-C57E-4C39-9356-E57DEB0BF2AC",
      "file": "file:///Users/example/Documents/Outlines/Inbox.opml",
      "title": "Inbox",
      "folder": "Outlines",
      "modified": "2026-11-13T09:30:00.125Z"
    }
  ]
}
FieldMeaning
versionResponse format identifier checked by the client, distinct from the app release number.
requestIDCanonical uppercase request UUID.
statussuccess or error.
documentsDestinations. An empty success array means a successfully read, empty library.
documentIDStable UUID, or null for a legacy outline. Prefer it for targeting existing endpoints; it survives moves and renames.
fileAbsolute file URL; fallback target when the document ID is null. A reference does not grant direct filesystem access.
titleDisplay title; not unique.
folderDisplay location including library root name, to distinguish equal titles; not a stable identity or machine-readable path.
modifiedISO-8601 UTC timestamp with millisecond precision, or null when unavailable. Accept timestamps with or without fractional seconds and honour timezone offsets.

Do not assume a particular sort order or unique titles/UUIDs across copied files. Retain file references for disambiguation. Ordinary outlines from local and available, enabled iCloud libraries are included. Templates, snippets and the generated Library Index are excluded. Ad hoc external files outside the managed libraries are not listed. An iCloud placeholder may still need downloading when opened or edited; a list entry does not guarantee a later capture will save successfully. Outline content is not exported.

Error response

{
  "version": 2,
  "requestID": "2BD4614C-240D-40A5-B69F-A30730D3AC54",
  "status": "error",
  "documents": [],
  "error": {
    "code": "unsupported-version",
    "message": "This Traverse build supports document-list protocol version 2."
  }
}

Errors always have an empty documents array and a non-empty code/message. Success omits error. Check status rather than interpreting an error as an empty library.

CodeMeaning
unsupported-versionThe requested protocol version is unsupported. The response uses the server's current version.
library-unavailableA configured library root cannot be read. Check its location and permissions before retrying.
list-failedAnother document-list failure. Present the message and allow retry.

A missing/invalid request UUID cannot address a response pasteboard. Traverse instead uses the existing optional x-error callback with errorCode=missing-request, or beeps if absent. If the pasteboard transport itself fails, a reply cannot be delivered there: an optional x-error callback receives response-unavailable, or the client times out. Older Traverse builds can also produce no reply. Timeouts, malformed JSON and mismatched request/version IDs are client-side failures, distinct from a server error response. Callback behavior for other endpoints is unchanged.

Standalone example

List Traverse documents uses AppKit/Foundation and this wire format, without a shared Traverse model dependency:

swift list-traverse-documents.swift

It prints JSON. Exit status is 0 for success, 2 for a valid server error reply, or 1 for a local launch/transport/validation failure (reported on stderr).

Requires macOS and Xcode Command Line Tools. Sandboxed clients must meet their own sandbox requirements for app launching and named-pasteboard access. No network access, credentials or access to Traverse's sandbox container is part of the protocol.

Scope and limitations

Clients should ignore unknown fields and reject unsupported response formats. Node enumeration, subscriptions, pagination and write acknowledgements are outside this protocol.

Runnable document-list client

Save this as list-traverse-documents.swift and run swift list-traverse-documents.swift. It is a complete client, using only AppKit and Foundation. It writes JSON to stdout and local failures to stderr. It does not modify library content.

#!/usr/bin/env swift
// macOS example client. Requires Xcode Command Line Tools; no third-party libraries.
// Run: swift list-traverse-documents.swift
import AppKit
import Foundation

let supportedVersion = 2
let requestedVersion = String(supportedVersion)

Task { @MainActor in
    let requestID = UUID()
    let board = NSPasteboard(name: NSPasteboard.Name("com.habitualshaker.traverse.documents." + requestID.uuidString))
    let type = NSPasteboard.PasteboardType("com.habitualshaker.traverse.document-list")
    var exitStatus: Int32 = 1
    do {
        let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.habitualshaker.Traverse")
            .sorted { ($0.launchDate ?? .distantPast) > ($1.launchDate ?? .distantPast) }
            .first?.bundleURL ?? NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.habitualshaker.Traverse")
        guard let app else { throw ClientError("Install Traverse 1.4.2 or later first.") }
        var url = URLComponents()
        url.scheme = "traverse"
        url.host = "x-callback-url"
        url.path = "/listDocuments"
        url.queryItems = [URLQueryItem(name: "request", value: requestID.uuidString), URLQueryItem(name: "version", value: requestedVersion)]
        let configuration = NSWorkspace.OpenConfiguration()
        configuration.activates = false
        _ = try await NSWorkspace.shared.open([url.url!], withApplicationAt: app, configuration: configuration)
        let clock = ContinuousClock()
        let deadline = clock.now.advanced(by: .seconds(10))
        var responseData: Data?
        while clock.now < deadline {
            if let data = board.data(forType: type) { responseData = data; break }
            try await Task.sleep(for: .milliseconds(100))
        }
        guard let data = responseData else { throw ClientError("Timed out waiting for Traverse. Check that the running build supports listDocuments.") }
        guard let response = try JSONSerialization.jsonObject(with: data) as? [String: Any],
              (response["version"] as? Int) == supportedVersion,
              let returnedID = response["requestID"] as? String, UUID(uuidString: returnedID) == requestID,
              let status = response["status"] as? String,
              let documents = response["documents"] as? [[String: Any]] else {
            throw ClientError("Unrecognised document-list response or request identity.")
        }
        switch status {
        case "success":
            guard response["error"] == nil else { throw ClientError("Unexpected error in a success response.") }
            exitStatus = 0
        case "error":
            guard documents.isEmpty, let error = response["error"] as? [String: Any],
                  let code = error["code"] as? String, !code.isEmpty,
                  let message = error["message"] as? String, !message.isEmpty else {
                throw ClientError("Malformed error response.")
            }
            exitStatus = 2
        default: throw ClientError("Unrecognised document-list status.")
        }
        let output = try JSONSerialization.data(withJSONObject: response, options: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes])
        FileHandle.standardOutput.write(output + Data("\n".utf8))
    } catch {
        FileHandle.standardError.write(Data((error.localizedDescription + "\n").utf8))
    }
    board.releaseGlobally()
    exit(exitStatus)
}
RunLoop.main.run()

struct ClientError: LocalizedError {
    let message: String
    init(_ message: String) { self.message = message }
    var errorDescription: String? { message }
}