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
| Endpoint | Purpose | Result |
|---|---|---|
traverse://open | Open an outline or node | Navigation; no callback result |
createOutline | Create a new library outline | Write-result callback |
append | Append parsed outline content | Write-result callback |
addNode | Add a node or parse content, with optional attributes | Write-result callback |
getCurrentNode | Read Traverse's current internal node | Node callback; x-success required |
getNode | Read a specified node | Node callback |
getNodeAttributes | Read normalized attributes | JSON in an attributes callback parameter |
updateNode | Update text, note or attributes | Node callback |
listDocuments | Discover library outlines | JSON 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-UUIDUse 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=plainTextRequires 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=markdownSupply 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%20openSupply 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-URLRequires 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-URLReturns 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-URLReturns 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¬e=NOTE&appendNote=trueSupply 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: WaitingNames 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,documentandfile, withnode/urlwhen a new node exists. - Node-result callbacks for getNode and updateNode also include
textandnote. - getNodeAttributes returns the
attributesJSON query value. - Error callbacks receive
errorCodeanderrorMessage. 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 code | Situation |
|---|---|
missing-title | createOutline has no title parameter |
missing-document | No document target can be resolved, including selection fallback |
missing-node | No node reference can be decoded |
missing-success / invalid-success | Required callback is absent, or a callback URL cannot be built |
no-current-node | No internal node is selected for getCurrentNode |
create-failed, append-failed, add-node-failed | The corresponding write failed |
get-node-failed, get-attributes-failed, update-node-failed | The 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=2request 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-UUIDPoll its data using this custom type:
com.habitualshaker.traverse.document-listTraverse 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"
}
]
}| Field | Meaning |
|---|---|
version | Response format identifier checked by the client, distinct from the app release number. |
requestID | Canonical uppercase request UUID. |
status | success or error. |
documents | Destinations. An empty success array means a successfully read, empty library. |
documentID | Stable UUID, or null for a legacy outline. Prefer it for targeting existing endpoints; it survives moves and renames. |
file | Absolute file URL; fallback target when the document ID is null. A reference does not grant direct filesystem access. |
title | Display title; not unique. |
folder | Display location including library root name, to distinguish equal titles; not a stable identity or machine-readable path. |
modified | ISO-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.
| Code | Meaning |
|---|---|
unsupported-version | The requested protocol version is unsupported. The response uses the server's current version. |
library-unavailable | A configured library root cannot be read. Check its location and permissions before retrying. |
list-failed | Another 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.swiftIt 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 }
}