Create and script actions for the Drafts app. Use this skill when the user needs help creating custom Drafts actions, understanding action steps, using template tags, scripting with JavaScript, or configuring action workflows for automation and productivity.
This skill provides comprehensive guidance for creating custom actions in Drafts, the text capture and automation app for iOS and macOS. Actions are powerful automation workflows that combine multiple steps to process text, integrate with external services, and automate repetitive tasks.
Actions in Drafts are automation workflows consisting of one or more sequential steps. Each action can:
Every action has three core components:
Actions can be triggered via:
drafts://x-callback-url/runAction)To create an action through the Drafts interface:
iOS:
macOS:
Actions can be created dynamically via JavaScript scripts within Drafts:
// Get or create an action group
let group = ActionGroup.find("My Actions");
// Create new action
let action = Action.create();
action.name = "My Custom Action";
// Add a script step
let step = action.addStep();
step.type = "script";
step.script = `
let content = draft.content.toUpperCase();
draft.content = content;
draft.update();
`;
// Add to action group
action.actionGroup = group;
action.update();
Actions can be shared via URL schemes:
drafts://action?data=[URL-encoded-JSON]Actions consist of sequential steps from four categories:
Built-in iOS/macOS integration steps:
External service integrations:
File Storage:
Task Management:
Notes & Documentation:
Email Services:
Publishing:
Data:
Internal Drafts operations:
Scripting and advanced automation:
Script: Execute JavaScript with Drafts API access
script.complete() when async work finishesInclude Action: Reuse steps from another action (prevents duplication)
Run Shortcut: Execute Apple Shortcuts with optional response waiting
Run AppleScript: macOS-only AppleScript execution
HTML Preview: Display interactive HTML with forms that send data back to actions
Prompt: Collect user input with customizable buttons and text fields
Callback URL: Send requests to apps supporting x-callback-url protocol
Open URL: Launch web URLs or app-specific URL schemes
Configured Value: Create user-configurable action parameters
HTMLPreview allows you to create custom HTML-based user interfaces that go beyond standard prompts. This is useful for complex forms, multi-step wizards, draft selection interfaces, and any interaction requiring custom styling.
The reliable pattern for HTMLPreview in a Script step:
var preview = HTMLPreview.create();
preview.hideInterface = true; // Hide default toolbar for custom buttons
if (preview.show(html)) {
var vals = context.previewValues["formValues"];
if (vals && vals.choice) {
// Process the user's choice
}
} else {
// User cancelled
context.cancel();
}
context.previewValues to access data sent from HTMLpreview.show() return value - true if continued, false if cancelledDrafts.send(key, value) in HTML JavaScript to send dataDrafts.continue() to close the preview and continue execution<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, sans-serif;
padding: 20px;
background: #fff;
color: #000;
}
@media (prefers-color-scheme: dark) {
body { background: #1c1c1e; color: #f5f5f7; }
}
button {
display: block;
width: 100%;
padding: 14px;
margin: 10px 0;
border-radius: 12px;
font-size: 16px;
}
.primary {
background: #007aff;
color: white;
border: none;
}
</style>
</head>
<body>
<h1>Choose an Option</h1>
<button class="primary" onclick="choose('yes')">Yes</button>
<button onclick="choose('no')">No</button>
<script>
function choose(choice) {
Drafts.send("formValues", { choice: choice });
Drafts.continue();
}
</script>
</body>
</html>
For forms with multiple fields:
<form id="data-form">
<input type="text" id="title" value="">
<textarea id="notes"></textarea>
<input type="checkbox" id="flagged">
</form>
<button onclick="submitForm()">Submit</button>
<script>
function submitForm() {
var form = document.getElementById('data-form');
var data = {};
for (var e of form.elements) {
if (e.type === 'checkbox') {
data[e.id] = e.checked;
} else if (e.id) {
data[e.id] = e.value;
}
}
Drafts.send("formValues", data);
Drafts.continue();
}
</script>
Then in your script:
if (preview.show(html)) {
var vals = context.previewValues["formValues"];
var title = vals.title;
var notes = vals.notes;
var flagged = vals.flagged;
}
You can show multiple HTMLPreview prompts in a loop:
var items = Draft.query("", "inbox", [], [], "created", false);
for (var i = 0; i < items.length; i++) {
var html = buildPromptHTML(items[i], i + 1, items.length);
var preview = HTMLPreview.create();
preview.hideInterface = true;
if (preview.show(html)) {
var vals = context.previewValues["formValues"];
if (vals.choice === "stop") break;
// Process choice...
} else {
break;
}
}
Always escape user content before inserting into HTML:
function escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
var html = `<div>${escapeHtml(draft.content)}</div>`;
preview.actionLog - use context.previewValues insteadDrafts.continue() - preview won't close without itpreview.show() return - user may have cancelledlet/const across multiple Script steps - causes duplicate variable errorsSee references/htmlpreview-forms-reference.md for complete documentation including dark mode CSS, button styles, progress bars, and full working examples.
Drafts uses a lightweight template engine with [[tag]] syntax for dynamic content.
Identifier Tags:
[[uuid]]: Unique draft identifier[[permalink]]: Shareable draft URLContent Tags:
[[draft]]: Full draft text[[title]]: First line of draft[[body]]: All lines after first line[[safe_title]]: First line with path-unsafe characters removed (\/:*?<>|#)[[selection]]: Currently selected text[[selection_only]]: Selected text or empty string if no selectionLocation Tags:
[[latitude]], [[longitude]]: Current location[[created_latitude]], [[created_longitude]]: Location when draft was created[[modified_latitude]], [[modified_longitude]]: Location when draft was last modifiedMetadata Tags:
[[tags]]: Comma-separated tag list[[line|n]]: Specific line number (e.g., [[line|2]] for second line)[[line|n..m]]: Line range (e.g., [[line|2..5]])Two formatting systems available:
strftime formats (traditional):
[[date|%Y-%m-%d]] → 2025-10-21
[[date|%B %d, %Y]] → October 21, 2025
[[created_date|%Y-%m-%d]] → Creation date
[[modified_date|%Y-%m-%d]] → Modification date
DateFormatter patterns (localized):
[[date|=shortDate]] → 10/21/25
[[date|=longDate]] → October 21, 2025
[[date|=iso8601]] → 2025-10-21T14:30:00Z
[[date|~yyyy-MM-dd]] → 2025-10-21
Modify dates relative to current time:
[[date|+1 day|%Y-%m-%d]] → Tomorrow
[[date|-1 week|%Y-%m-%d]] → Last week
[[date|+1 year|%Y-%m-%d]] → Next year
[[date|+3 hours|%H:%M]] → 3 hours from now
Case transformation:
[[title:upper]]: UPPERCASE[[title:lower]]: lowercaseLength limiting:
[[title:max=50]]: Truncate to 50 charactersURL encoding:
{{ }} syntax for URL encoding: {{title}}Markdown conversion:
%% %% syntax to convert to Markdown: %%draft%%Create custom tags in scripts:
draft.setTemplateTag("word_count", draft.content.split(/\s+/).length);
// Use as [[word_count]] in templates
Load external templates from iCloud Drive:
[[template|path/to/template.txt]]
Prevent template evaluation with backslash:
\[[title]] → Renders as literal "[[title]]"
Drafts provides a complete JavaScript runtime (ECMAScript 6) with extensive APIs.
Draft object:
// Access current draft
draft.content // Full text content
draft.title // First line
draft.body // All lines after first
draft.tags // Array of tags
draft.isFlagged // Boolean flag status
draft.isArchived // Boolean archive status
draft.createdAt // Date created
draft.modifiedAt // Date modified
// Modify draft
draft.content = "New content";
draft.addTag("important");
draft.removeTag("old");
draft.update(); // Save changes
// Template processing
let processed = draft.processTemplate("[[title]] - [[date]]");
// Custom template tags
draft.setTemplateTag("custom", "value");
Editor object:
// Access editor state
editor.getText() // Current text
editor.getSelectedText() // Selected text
editor.getSelectedRange() // [start, length]
editor.getTextInRange(0, 10) // Get specific range
// Modify editor
editor.setText("New text");
editor.setSelectedText("Replacement");
editor.setSelectedRange(0, 5); // Select range
editor.activate(); // Focus editor
App object:
// App state
app.currentWorkspace // Active workspace
app.isIdleDisabled // Idle timer state
// User interaction
app.displayInfoMessage("Info");
app.displayWarningMessage("Warning");
app.displayErrorMessage("Error");
// Open URLs
app.openURL("https://example.com");
Context object:
// Access action context
context.callbackURL // x-callback-url that triggered action
context.configuredValues // User-configured values for action
// Example: Access configured value
let folderName = context.configuredValues["folderName"];
Query drafts:
// Find drafts
let all = Draft.query("", "all", [], [], "accessed");
let tagged = Draft.query("", "inbox", ["important"], [], "created");
// Parameters: content, filter, tags, omit_tags, sort
// filter: "inbox", "archive", "flagged", "all"
// sort: "created", "modified", "accessed"
Create new drafts:
let d = Draft.create();
d.content = "New draft content";
d.addTag("auto-created");
d.update();
Process multiple drafts:
let drafts = Draft.query("todo", "inbox", [], [], "created");
for (let d of drafts) {
d.addTag("processed");
d.update();
}
For HTTP requests, delays, or async operations:
// Enable "Allow asynchronous execution" in Script step settings
// HTTP request example
let http = HTTP.create();
let response = http.request({
"url": "https://api.example.com/data",
"method": "POST",
"encoding": "json", // CRITICAL: Required to serialize data as JSON
"data": {
"title": draft.title
},
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_API_KEY"
}
});
if (response.success) {
app.displayInfoMessage("Posted successfully");
} else {
app.displayErrorMessage("Request failed");
context.fail();
}
// REQUIRED: Signal completion
script.complete();
Important HTTP Request Parameters:
"encoding": "json" - CRITICAL: Tells Drafts to automatically JSON-serialize the data object. Without this, the request body will be empty!"data" - Pass as a JavaScript object, NOT a JSON string. Drafts will serialize it automatically when encoding: "json" is set."headers" - Object containing HTTP headers"method" - HTTP verb ("GET", "POST", "PUT", "DELETE", etc.)Common mistake: Omitting "encoding": "json" or trying to JSON.stringify() the data manually. Always use "encoding": "json" with an object in the "data" field.
Mail integration:
let mail = Mail.create();
mail.toRecipients = ["user@example.com"];
mail.subject = draft.title;
mail.body = draft.body;
mail.isHTML = false;
let success = mail.send();
if (!success) {
context.fail();
}
Dropbox integration:
let db = Dropbox.create();
let success = db.write(
`/notes/${draft.title}.txt`,
draft.content,
"add", // Mode: "add", "overwrite"
false // Not a folder
);
if (!success) {
app.displayErrorMessage("Dropbox write failed");
context.fail();
}
Actions support user-configurable values for flexibility without editing.
In templates:
[[folderName]]
In scripts:
let folder = context.configuredValues["folderName"];
Users configure actions via context menu:
Only actions with configured values show "Configure" option.
context.fail() for failuresapp.displayInfoMessage() for statusdraft.update() after modificationsConvert draft to task/note in external service:
Steps:
[[title]][[body]]Save draft as file with timestamp:
Steps:
[[date|%Y-%m-%d]]-[[safe_title]].md[[draft]]Send formatted email from template:
Steps:
user@example.com[[title]]Post to multiple platforms simultaneously:
Steps:
Use script to check conditions:
// Check if draft has specific tag
if (draft.tags.includes("urgent")) {
// Create high-priority task
let reminder = Reminder.create();
reminder.title = draft.title;
reminder.priority = 1;
reminder.update();
} else {
// Regular processing
draft.addTag("normal");
draft.update();
}
Transform draft content in-place:
// Convert to uppercase
draft.content = draft.content.toUpperCase();
draft.update();
// Add line numbers
let lines = draft.content.split("\n");
let numbered = lines.map((line, i) => `${i+1}. ${line}`).join("\n");
draft.content = numbered;
draft.update();
// Word count
let words = draft.content.split(/\s+/).length;
draft.setTemplateTag("count", words);
app.displayInfoMessage(`Word count: ${words}`);
Process multiple drafts matching criteria:
// Find all flagged drafts
let drafts = Draft.query("", "flagged", [], [], "created");
for (let d of drafts) {
// Add tag and unflag
d.addTag("reviewed");
d.isFlagged = false;
d.update();
}
app.displayInfoMessage(`Processed ${drafts.length} drafts`);
Complete working example calling the Claude API to translate draft content:
// Get the current draft content
const originalText = draft.content;
// Your Anthropic API key (set this in Drafts credentials)
const credential = Credential.create("Anthropic API", "Enter your Anthropic API key");
credential.addPasswordField("apiKey", "API Key");
credential.authorize();
const apiKey = credential.getValue("apiKey");
if (!apiKey) {
alert("API key not found. Please set up your Anthropic API key.");
context.fail();
}
// Claude API endpoint
const endpoint = "https://api.anthropic.com/v1/messages";
// Prepare the API request
const http = HTTP.create();
const requestData = {
"model": "claude-3-5-haiku-20241022",
"max_tokens": 4096,
"messages": [
{
"role": "user",
"content": `Translate the following text to English. Only provide the translation, no explanations or additional text:\n\n${originalText}`
}
]
};
// Make the API call
const response = http.request({
"url": endpoint,
"method": "POST",
"encoding": "json", // CRITICAL: Required for JSON serialization
"data": requestData,
"headers": {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01"
}
});
// Check if the request was successful
if (response.success) {
const responseData = JSON.parse(response.responseText);
const translation = responseData.content[0].text;
// Append translation below original text
draft.content = originalText + "\n\n---\n\n**English Translation:**\n\n" + translation;
draft.update();
app.displaySuccessMessage("Translation completed!");
} else {
// Try to parse error message
let errorMsg = "Translation failed. Status code: " + response.statusCode;
try {
const errorData = JSON.parse(response.responseText);
if (errorData.error && errorData.error.message) {
errorMsg += "\n\nError: " + errorData.error.message;
}
} catch (e) {
errorMsg += "\n\nResponse: " + response.responseText;
}
alert(errorMsg);
context.fail();
}
Key Points:
This skill includes reference documentation with detailed information:
Comprehensive documentation of all action step types with examples and configuration options. Load this reference when working with specific step types or needing detailed parameter information.
Detailed JavaScript API reference covering all major objects (Draft, Editor, App, HTTP, etc.) with method signatures, parameters, and return values. Load when scripting complex actions or working with specific API features.
Complete reference for Drafts template tag syntax covering all built-in tags (identifier, content, location, date/time, utility), formatting options (strftime, DateFormatter), date adjustment, special markup, and escaping. Load when working with templates or needing detailed syntax beyond the core template concepts in this skill.
Comprehensive guide to creating custom HTML-based user interfaces using HTMLPreview. Covers the working pattern for context.previewValues, form data collection, sequenti
Google Workspace CLI for Gmail, Calendar, Drive, Contacts, Sheets, and Docs.
Manage Apple Notes via the `memo` CLI on macOS (create, view, edit, delete, search, move, and export notes). Use when a user asks OpenClaw to add a note, list notes, search notes, or manage note folders.
Work with Obsidian vaults (plain Markdown notes) and automate via obsidian-cli.
Use when you need to control Slack from OpenClaw via the slack tool, including reacting to messages or pinning/unpinning items in Slack channels or DMs.
Manage Apple Reminders via remindctl CLI (list, add, edit, complete, delete). Supports lists, date filters, and JSON/plain output.
Manage Trello boards, lists, and cards via the Trello REST API.
Category:productivity