# Pasteon Workflow Protocol

This document is the machine-readable reference for creating, explaining, or generating Pasteon Workflows. It describes the current Workflow model implemented by Pasteon for macOS.

Canonical human-readable guide: https://pasteon.app/workflows

## Overview

A Workflow turns a selected clipboard item into a reusable Pasteon Action.

Two editor modes are supported:

- `steps`: a linear visual pipeline of conditions, transforms, scripts, and one final output.
- `code`: an inline or external script that reads versioned JSON from stdin and writes protocol JSON to stdout.

Two execution modes are supported:

- `simple`: execute immediately and return one result.
- `scriptFilter`: return up to 20 candidate Actions during `filter`, then compute only the selected result during `execute`.

## WorkflowDefinition

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `id` | UUID | yes | Stable Workflow identifier. |
| `name` | string | yes | Display name in Settings and the Action list. |
| `summary` | string | no | User-facing explanation. |
| `symbolName` | SF Symbol name | yes | Action icon. |
| `isEnabled` | boolean | yes | Disabled Workflows remain saved but do not appear as Actions. |
| `sortOrder` | number | yes | Ordering after built-in Primary Actions. |
| `mode` | `simple` or `scriptFilter` | yes | Execution model. |
| `supportedTypes` | PasteType[] | yes | Clipboard types where the Workflow may appear. |
| `editorMode` | `steps` or `code` | no | Missing values from older files are treated as `code`. |
| `script` | ScriptConfiguration | code only | Script source, interpreter, arguments, working directory, and timeouts. |
| `steps` | WorkflowStepDefinition[] | steps only | Ordered visual pipeline. |
| `variables` | WorkflowVariableDefinition[] | no | Reusable Text and Secret values. |
| `requiresTrustConfirmation` | boolean or null | imported code | Imported code is disabled until the user confirms trust. |

Supported PasteType values include `text`, `markdown`, `html`, `json`, `url`, `color`, `date`, `timestamp`, `fileURL`, `imageURL`, `videoURL`, `image`, and `appObject`.

## Workflow variables

Variables let a Workflow reuse configuration without hard-coding it in every Template or script.

### WorkflowVariableDefinition

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `id` | UUID | yes | Stable identity used to associate Secret data with a Keychain entry. |
| `name` | string | yes | Case-sensitive lookup name. |
| `kind` | `text` or `secret` | yes | Storage behavior. |
| `value` | string | text only | Text value stored with the Workflow. Secret exports always contain an empty value. |

Variable names must:

- match `[A-Za-z_][A-Za-z0-9_]*`;
- be unique inside one Workflow;
- be referenced with exactly the same letter case.

Example:

```json
{
  "variables": [
    {
      "id": "C3EEAF0E-0AD8-4A26-924C-CB867088D955",
      "name": "API_BASE_URL",
      "kind": "text",
      "value": "https://api.example.com"
    },
    {
      "id": "7E05B2EE-E745-4558-A267-E63B7D1E2235",
      "name": "API_TOKEN",
      "kind": "secret",
      "value": ""
    }
  ]
}
```

### Text variables

- Stored in the Workflow data.
- Included when the Workflow is copied or exported.
- Suitable for endpoints, formats, prefixes, project names, and other non-sensitive configuration.

### Secret variables

- Stored in the macOS Keychain under the Workflow ID and variable ID.
- Never exported with their value.
- Must be entered again after importing a Workflow.
- Are deleted from Keychain when the variable or Workflow is removed.
- Are supplied to the Workflow only at runtime.

Copying a Workflow creates new variable IDs. Secret values therefore do not silently share Keychain entries between the original and its copy.

### Variables in visual Steps

Use this exact placeholder syntax in Template content or an output file name:

```text
{{variables.API_BASE_URL}}
{{variables.API_TOKEN}}
```

Built-in placeholders are:

- `{{input}}`: current text from the preceding step.
- `{{sourceApp}}`: source application bundle identifier, or an empty string.
- `{{targetApp}}`: frontmost target application bundle identifier, or an empty string.
- `{{clipboardType}}`: current PasteType raw value.
- `{{date}}`: current date and time in ISO 8601 format.
- `{{variables.NAME}}`: resolved user variable.

An absent or empty variable resolves to an empty string.

### Variables in Code Workflows

Resolved variables are included in the top-level `variables` object sent to stdin:

```js
const payload = JSON.parse(input);
const baseURL = payload.variables.API_BASE_URL ?? "";
const token = payload.variables.API_TOKEN ?? "";
```

Both Text and Secret values are available during `filter` and `execute`.

Do not write Secret values to stdout or stderr. stdout must contain only protocol JSON, and stderr may be visible in diagnostics.

## ScriptConfiguration

| Field | Type | Meaning |
| --- | --- | --- |
| `source` | `inline` or `external` | Inline stores code in the Workflow; external points to a file. |
| `code` | string | Inline script body. |
| `scriptPath` | file path | External script path; tilde is expanded. |
| `interpreterPath` | executable path | Interpreter such as `/bin/zsh` or a Node executable. |
| `arguments` | string[] | Passed as separate process arguments. No shell joining or wildcard expansion. |
| `workingDirectory` | directory path | Process current directory. |
| `filterTimeout` | seconds | Clamped to 1–120; default 5. |
| `executeTimeout` | seconds | Clamped to 1–120; default 15. |

## JSON sent to stdin

```json
{
  "version": 1,
  "workflowID": "5F604A89-7AC2-45D9-B48C-BB540B60CA67",
  "phase": "execute",
  "paste": {
    "id": "D0A4E45A-61F6-4DAF-BC1E-4E17A23AA5C0",
    "type": "json",
    "text": "{\"name\":\"Pasteon\"}",
    "sourceAppBundleIdentifier": "com.apple.Safari",
    "representations": [
      {
        "type": "public.utf8-plain-text",
        "value": "{\"name\":\"Pasteon\"}",
        "path": null,
        "fileName": null,
        "fileSize": 18,
        "temporary": false
      }
    ]
  },
  "targetApp": {
    "bundleIdentifier": "com.apple.dt.Xcode",
    "name": "Xcode"
  },
  "directories": {
    "input": "/tmp/Pasteon/WorkflowInputs/RUN_ID",
    "output": "~/Library/Application Support/Pasteon/WorkflowOutputs/RUN_ID"
  },
  "variables": {
    "API_BASE_URL": "https://api.example.com",
    "API_TOKEN": "secret supplied at runtime"
  },
  "actionID": null,
  "argument": null
}
```

### Input fields

- `version`: protocol version; currently `1`.
- `workflowID`: stable Workflow UUID.
- `phase`: `filter` or `execute`.
- `paste.id`: selected history item UUID.
- `paste.type`: derived PasteType.
- `paste.text`: text representation when available.
- `paste.sourceAppBundleIdentifier`: app that originally copied the item.
- `paste.representations`: pasteboard representations.
- `targetApp.bundleIdentifier`: frontmost target app bundle identifier.
- `targetApp.name`: target app display name.
- `directories.input`: temporary input directory, removed after the run.
- `directories.output`: directory for files returned by the Workflow.
- `variables`: resolved Text and Secret values keyed by variable name.
- `actionID`: selected Filter candidate ID during execute, otherwise null.
- `argument`: selected Filter candidate JSON argument, otherwise null.

## Reading input in common languages

Every Code Workflow must read the complete stdin stream before parsing JSON. Write exactly one protocol JSON object to stdout. Send logs and diagnostics to stderr.

Pasteon does not bundle language runtimes or `jq`. Set `interpreterPath` to an executable that exists on the current Mac. Homebrew paths commonly start with `/opt/homebrew/bin` on Apple silicon and `/usr/local/bin` on Intel Macs. Use `which node`, `which python3`, or the equivalent to find the actual path.

All examples below read:

- `payload.paste.text`;
- the user-defined `PREFIX` variable from `payload.variables`;
- and return a valid Simple Workflow text result.

### JavaScript / Node.js

Example interpreter: `/opt/homebrew/bin/node`

```js
const fs = require("node:fs");

const payload = JSON.parse(fs.readFileSync(0, "utf8"));
const text = payload.paste?.text ?? "";
const prefix = payload.variables?.PREFIX ?? "";

process.stdout.write(JSON.stringify({
  version: 1,
  result: { type: "text", value: prefix + text }
}));
```

### Python 3

Example interpreter: `/opt/homebrew/bin/python3`

```python
import json
import sys

payload = json.load(sys.stdin)
text = payload.get("paste", {}).get("text") or ""
prefix = payload.get("variables", {}).get("PREFIX", "")

json.dump({
    "version": 1,
    "result": {"type": "text", "value": prefix + text}
}, sys.stdout)
```

### Ruby

Example interpreter: `/opt/homebrew/bin/ruby`

```ruby
require "json"

payload = JSON.parse($stdin.read)
text = payload.dig("paste", "text") || ""
prefix = payload.dig("variables", "PREFIX") || ""

$stdout.write(JSON.generate({
  version: 1,
  result: { type: "text", value: prefix + text }
}))
```

### PHP

Example interpreter: `/opt/homebrew/bin/php`

```php
<?php
$payload = json_decode(stream_get_contents(STDIN), true);
$text = $payload["paste"]["text"] ?? "";
$prefix = $payload["variables"]["PREFIX"] ?? "";

echo json_encode([
    "version" => 1,
    "result" => ["type" => "text", "value" => $prefix . $text]
]);
```

### Swift

Example interpreter: `/usr/bin/swift`

Swift is available when the relevant Apple developer tools are installed.

```swift
import Foundation

let data = FileHandle.standardInput.readDataToEndOfFile()
let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let paste = payload?["paste"] as? [String: Any]
let variables = payload?["variables"] as? [String: String]
let text = paste?["text"] as? String ?? ""
let prefix = variables?["PREFIX"] ?? ""

let response: [String: Any] = [
    "version": 1,
    "result": ["type": "text", "value": prefix + text]
]
let output = try JSONSerialization.data(withJSONObject: response)
FileHandle.standardOutput.write(output)
```

### Zsh with jq

Interpreter: `/bin/zsh`

Zsh can read the raw stdin stream, but this example requires a separately installed `jq` executable for reliable JSON parsing and encoding.

```zsh
#!/bin/zsh
set -euo pipefail

payload="$(cat)"
text="$(printf '%s' "$payload" | jq -r '.paste.text // ""')"
prefix="$(printf '%s' "$payload" | jq -r '.variables.PREFIX // ""')"

jq -n --arg value "$prefix$text" '{
  version: 1,
  result: {type: "text", value: $value}
}'
```

## Representation fields

| Field | Type | Meaning |
| --- | --- | --- |
| `type` | UTI string | Pasteboard type, such as `public.utf8-plain-text`, `public.file-url`, or `public.png`. |
| `value` | string or null | Inline UTF-8 text or original file URL string. |
| `path` | file path or null | File or materialized binary representation. |
| `fileName` | string or null | Suggested file name. |
| `fileSize` | integer | Byte count. |
| `temporary` | boolean | Whether Pasteon created and owns this temporary file. |

## Script Filter response

During `filter`, return:

```json
{
  "version": 1,
  "actions": [
    {
      "id": "swift",
      "title": "Generate Swift Model",
      "subtitle": "Create Codable types",
      "icon": "swift",
      "argument": {
        "language": "swift"
      }
    }
  ]
}
```

Rules:

- Maximum 20 candidates.
- `id` must be unique within the response.
- `title` is required.
- `subtitle` and `icon` are optional.
- `argument` may be any JSON value and is returned unchanged during execute.
- Filter should return metadata, not precompute every final result.

## Execute response

### Text

```json
{
  "version": 1,
  "result": {
    "type": "text",
    "value": "Ready to paste"
  }
}
```

### Files

Files must be created inside `directories.output`. Return relative paths:

```json
{
  "version": 1,
  "result": {
    "type": "files",
    "paths": ["GeneratedModel.swift"]
  }
}
```

### Open URL

```json
{
  "version": 1,
  "result": {
    "type": "openURL",
    "url": "https://example.com"
  }
}
```

## Runtime rules

- stdout is reserved for one protocol JSON object.
- Write diagnostics to stderr.
- Non-zero exit status is a failure.
- Filter and Execute timeouts are clamped to 1–120 seconds.
- A single argument is limited to 64 KB.
- stdout is limited to 2 MB.
- stderr is limited to 256 KB.
- Switching Paste items, leaving Filter results, editing or deleting the Workflow, or closing the window cancels execution.
- Output directories older than 24 hours are removed during application cleanup.
- Scripts run locally with the current macOS user's permissions.
- Imported code requires trust confirmation before it can run.

Process environment variables:

- `PASTEON_WORKFLOW_ID`
- `PASTEON_WORKFLOW_PHASE`
- `PASTEON_INPUT_DIR`
- `PASTEON_OUTPUT_DIR`

User-defined Workflow variables are intentionally passed through stdin, not added to the process environment.

## Complete Node.js Script Filter example

```js
async function readStdin() {
  let input = "";
  process.stdin.setEncoding("utf8");
  for await (const chunk of process.stdin) input += chunk;
  return JSON.parse(input);
}

async function main() {
  const payload = await readStdin();
  const text = payload.paste?.text ?? "";
  const prefix = payload.variables.PREFIX ?? "";
  const token = payload.variables.API_TOKEN ?? "";

  if (payload.phase === "filter") {
    process.stdout.write(JSON.stringify({
      version: 1,
      actions: [
        {
          id: "prefix",
          title: "Add configured prefix",
          subtitle: "Uses the PREFIX Workflow variable",
          icon: "text.insert",
          argument: null
        }
      ]
    }));
    return;
  }

  // Use token for a local or remote operation if needed, but never log it.
  void token;

  process.stdout.write(JSON.stringify({
    version: 1,
    result: {
      type: "text",
      value: `${prefix}${text}`
    }
  }));
}

main().catch((error) => {
  process.stderr.write(`${error.stack ?? error.message}\n`);
  process.exit(1);
});
```

## Import and export

A `.pasteon-workflow` file is versioned JSON and may contain one Workflow or a Workflow list.

- Text variable values are included in exports.
- Secret variable definitions are included, but their values are cleared.
- Imported scripts are marked untrusted and disabled until confirmed.
- Review imported code because it executes locally with the current user's permissions.
