Actions ▸ Reference

Standard library reference

Every action that ships inside the app, section by section, with its own help. This page is generated from the library the app actually carries, so it lists what your copy has.

These are the actions the app builds from code at every launch. They are read-only: to change one, take a copy in the Actions pane, which lands above the library as one of your own and takes over that keyword.

Each is named by its keyword, which is what a pipeline writes, what a trigger names, and what a shortcut binds:

$1 | trim | upper
$2 | split , | first
$1 | addtz UTC | totz America/New_York | strftime "%F %T"

A few of these read the entry rather than the text handed to them — undo, ocr and the image filters — so they have to be the first filter in a pipeline. The image filters hand on a picture, so only another image filter, qr, data-uri, colors or describe-image reading it into text, or a step that acts on the entry — tag, untag, describe, forget, from — or open can follow one. qrcode goes the other way: it reads text and makes a picture. Nine of them, now, utcnow, tag-all, untag-all, copy, select, skip-copies, pause-copying-for and stop-speak, read nothing handed to them, which is what makes them somewhere to start: | now | strftime %H:%M works on an empty history.

skip-copies and pause-copying-for pause recording and start it again on their own — after the next few copies (| skip-copies 3), or after a while (| pause-copying-for 15m). They paste nothing and leave the clipboard alone, and like copy they only run when you start them, never from a trigger or the API. The menu bar's Pause Recording For offers the common ones.

open opens what it is handed the way ⌘O opens a row in the overlay — a link in your browser, copied files in their apps, text or a picture as a file of its own — or in the app you name after it: $1 | open Firefox, $3 | resize 50% | open Preview. It pastes nothing and leaves the clipboard alone. A trigger can run it, and so can the API and the Shortcuts app — for web, mail and Maps links and documents only, never an app or a script. An assistant over MCP can't run it.

copy, select and paste work on the app in front: copy and select read the text you have selected there, and paste pastes into it. Bound to a shortcut as a pipeline, they transform a selection where it is — | select | upper | paste. copy puts the selection on top of the history like any copy; select leaves the clipboard and the history exactly as they were. copypaste (paste --copy) pastes the same way and keeps what it pasted: on top of the history, and still on the clipboard — | select | upper | copypaste. They only run inside a pipeline, from a shortcut, the menu bar or the search bar — never from a trigger or the API — and need Accessibility permission. forget, seal, nopaste, tag, untag, describe, unfmt and html change no characters, so they can sit anywhere in a chain. tag, untag, describe and seal also leave out the paste, as nopaste does: filing or sealing a row isn't using it. So does a saved action that ends in > $name. And md-rich, rtf-rich and html-rich are the filters here that make formatting — they read the text as Markdown, RTF source or HTML source and put what that renders to on the clipboard, so it pastes as headings and bold rather than as the marks that describe them.

Twelve ask Apple Intelligence, on your Mac: proofread, summarize, rewrite, ask, headline and reply hand back text; extract a hash, tabulate a list of hashes and tasks and points a list; classify one of the labels you give it; and when a date. They are long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on your clipboard, where it waits rather than being pasted. A trigger can run them too. rewrite, ask, extract, classify and translate need an argument — a style, an instruction, fields, labels, a language — so the lists that run an action bare leave them out; save one as a pipeline action, | rewrite concise, to put it on a shortcut or in a trigger. They need macOS 26 with Apple Intelligence switched on, and say what is missing anywhere else — except when, which reads what the date filters understand on any Mac and only needs the model for the rest. translate is in the same position without the model: it uses macOS's own translation, and needs macOS 26.

They can ask a model you choose instead — Anthropic's, OpenAI's, or one on your Mac such as Ollama — which needs no Apple Intelligence and takes far longer texts, but sends the text to that model's server. translate and ? questions stay on your Mac.

One needs a chosen model: describe-image says what a picture shows — $1 | describe-image — or answers a question about it, $1 | describe-image "what error is this?", and sends the picture to that model's server, since Apple Intelligence reads only text. ocr gives back a screenshot's words; describe-image says what they mean. With describe, it makes a screenshot findable by what is in it: $1 | describe $($1 | describe-image).

Others read the words with what every Mac already has, no Apple Intelligence needed: extract-dates, extract-phones and extract-addresses, language, redact, which masks names, emails, phone and card numbers, SSNs, IBANs, IP addresses, tokens and API keys (redact secrets for just the keys, --ignore=ssn,iban to leave some alone), spellcheck and dedupe. These are ordinary actions, offered in the overlay and nameable by a trigger.

is and matches answer true or false — whether the text is one of the values you name, or matches a regex — from answers whether the entry was copied in one of the apps you name, | from Safari, from-window-named whether the title of the window it was copied in matches a regex, | from-window-named login checkout, from-window the same of the title kept with an entry, so $3 | from-window invoice asks about the third row, and is-kind whether it is an image, files, text or rtf. !, &&, || and ^^ combine those answers. With a step after it, a test stops the pipeline on false. They are what a trigger's pipeline condition is built from — | language | is fr || $(| language | is es) — and only run inside a pipeline.

General undo ocr forget seal nopaste skip-copies pause-copying-for speak stop-speak open tag tag-all untag untag-all describe allow-sync no-sync unfmt html eval fill copy select paste copypaste
Images resize crop rotate flip png jpeg fit strip remove-background qr data-uri colors qrcode
Text ++ trim collapse-spaces squeeze-blanks strip-html strip-ansi strip-control show-control tidy sort-lines sort-natural unique-lines reverse-lines number-lines join-lines lf crlf cr shuffle wrap unwrap smart-quotes guillemets straight-quotes count extract-urls extract-emails extract-dates extract-phones extract-addresses replace truncate language is matches from from-window-named from-window is-kind kind ! && || ^^ redact spellcheck lorem translate
Markdown md-quote md-unquote md-bullet md-numbered md-checklist md-code md-table md-escape md-link md-plain md-html md-rich
Lists split join item reverse continue restart history-paste stop index map first last filter reject sort unique dedupe sort-by flatten take drop zip
Case upper lower title sentence snake kebab camel pascal capitalize-lines alternating random-case
Encoding base64 base64-decode url-encode url-decode md5 sha256 regex-escape html-escape html-unescape html-numeric rtf-source rtf-rich html-rich shell-escape applescript-escape js-escape swift-escape
Math + - * / hex bin dec in-words convert round sum avg min max
JSON json json-compact json-escape json-hash to-json env
Data xml csv tsv keys values
Time now utcnow strftime addtz totz dateadd epoch
Web remove-url-tracking host url-scheme url-user url-port url-path url-query url-fragment https hex-color rgb hsl oklch slug share-link
Development jira-link jwt-decode mask-home dirname basename extension no-extension expand-tilde resolve-symlinks uuid
Apple Intelligence proofread summarize when rewrite ask headline extract classify tabulate tasks points reply describe-image
Fun upside-down spongebob-case

General

undoReads the entry

Go back to an earlier value

Reads what the entry the pipeline started from used to hold, instead of what it holds now. undo 1 is the value before the current one, undo 2 the one before that, and undo -1 the first value it ever held. With no number it goes back once.

This is the way back from a trigger that fired when it should not have: the text as it was copied is still in the entry, behind what the trigger made of it.

The value comes back whole: the text, how it was formatted, and the app's own data it carried — a Pages paragraph pastes back as Pages made it. A filter after it that changes the text leaves plain text, as any filter does; undo | unfmt asks for the text alone.

$1 | undo

A variable is put back by sending the result to itself, which is recorded like any other change — so an undo is itself undoable:

$signature | undo 1 > $signature

Because it reads the entry rather than the text, undo has to be the first filter in a pipeline; anything before it would have its work thrown away. Filters after it work on the older value as usual.

ocrReads the image

Read the text in an image

Hands on the words in a picture. Every image copied to the clipboard is read on its way into the history, and this is how what it said is got at:

$1 | ocr
$2 | ocr | trim > $address

That makes it the way to get a serial number, an error message or an address out of a screenshot without typing it out. What comes back is text like any other, so the rest of the library follows it:

$1 | ocr | join-lines ", "

The reading is also what makes a picture findable: typing an invoice number into the overlay finds the screenshot of the invoice, without ocr being run at all.

Because it reads the entry's picture rather than the text it was handed, ocr has to be the first filter in a pipeline — nothing produces an image for it to read, so anything before it would only have made text for it to ignore. For the same reason it is a filter you type rather than one the overlay offers: the list of actions only appears while the newest item is text, which is exactly when this one has nothing to read.

A picture with no words in it says so rather than handing back nothing, as does one copied before the app could read images, and one whose reading has not come back yet — it happens in the background, so a very large screenshot can take a moment after it is copied.

forgetHistory

Keep it out of the history

Passes the text through exactly as it was given, and asks for it not to be kept: the entry it ran on is dropped from the clipboard history, and nothing the run produced is put back there.

The text stays on the clipboard. Forgetting is about what PasteDaemon remembers, not about what you copied — you can still paste it, once, wherever you were going to.

Name it from a trigger to keep a whole shape of thing out of the history. A trigger that matches what your password manager copies, or an API token, or a one-time code, will drop every such copy the moment it is made:

Matches regex   ^[A-Za-z0-9_-]{32,}$
Actions         forget

It can also be run on something already copied — from the menu bar, from the overlay, or by hand — which is the way to take one thing back out again:

$1 | forget

A variable is never deleted by it: variables are written down on purpose and kept until you delete them, so $token | forget keeps the run out of the history and leaves ${token} alone.

It changes no text, so it can sit anywhere in a chain — $1 | trim | forget puts the trimmed text on the clipboard and keeps both it and the untrimmed original out of the history.

sealseal

Keep it in the history as a secret

Passes the text through exactly as it was given, and asks for the entry it is kept in to be sealed: encrypted with the same key as your secrets, and shown as •••••••• everywhere it appears. The row stays in the history — it is a clipping, not a variable — but nothing finds it by searching, and nothing reads it without you.

Choosing the row asks for Touch ID (or your login password) and then pastes it. One unlock covers every secret for as long as you set in General → Secrets.

Name it from a trigger to seal a whole shape of thing the moment it is copied — a token, a key, a card number:

Matches regex   ^(sk|ghp|xox[bp])[-_][A-Za-z0-9_-]{20,}$
Actions         seal

The copy stays on the clipboard, so you can still paste it where you were going to. Every earlier value of the entry goes, since the text as it was copied would otherwise sit behind the sealed one in the clear.

It can also be run on something already copied:

$1 | seal

Run that way it pastes nothing, as tag doesn't: sealing the row is the point, and the result only goes on the clipboard.

Where forget keeps a copy out of the history altogether, seal keeps it — locked. A variable is never sealed by it: a secret variable is made from the Variables pane, named with a !.

nopastenoPaste

Copy the result, don't paste it

Puts the result on the clipboard as usual and leaves the paste out, even with Automatically paste selection on. The text goes through unchanged, so it can sit anywhere in a command:

$3 | trim | nopaste
$1 | json | nopaste

Saved in an action — | tidy | nopaste — it holds the paste wherever the action runs: the search bar, the menu bar, a shortcut. ⌘C over a command in the search bar does the same for one run without writing it in.

tag, untag and describe hold the paste on their own, and so does a saved action that sends its result to a variable with > — it hands back the text it was given, which is nothing to paste.

It only stops the paste at the end. A paste step in the same command still pastes, since naming it is asking for it, and one inside a $( … ) holds nothing: that text is only spliced into the command around it.

skip-copiesskipCopies

Leave the next copies out of the history

Stops keeping what is copied for the next few copies, then starts again on its own — | skip-copies leaves out the next one, | skip-copies 3 the next three. For the password about to be copied, without having to remember to resume.

It pastes nothing and leaves the clipboard as it is. Recording comes back early with ⌘T or the menu bar, and the menu bar says how many copies are left.

It runs only when somebody starts it — the search bar, the menu bar, a shortcut — never from a trigger or the API, and the search bar's preview waits for ↩.

pause-copying-forpauseCopying

Pause recording for a while

Stops keeping what is copied for a length of time, then starts again on its own — | pause-copying-for 15m, | pause-copying-for 1h30m. The span is written as dateadd writes one; a bare number is minutes, and with nothing after it the pause lasts 15 minutes.

It pastes nothing and leaves the clipboard as it is. Recording comes back early with ⌘T or the menu bar, and the menu bar says when the pause ends. Like ⌘T's pause, it does not outlast a restart.

It runs only when somebody starts it — the search bar, the menu bar, a shortcut — never from a trigger or the API, and the search bar's preview waits for ↩.

speakspeak

Read it aloud

Reads the text aloud, in the voice chosen in System Settings → Accessibility → Spoken Content:

$1 | speak
$3 | speak fr
$1 | speak Samantha

A word after it picks another voice: a language — fr, pt-BR — for the best voice this Mac has in it, or a voice by its name. More voices can be downloaded in Spoken Content.

It pastes nothing and leaves the clipboard as it is: hearing a copy is not using it. The text goes on unchanged, so it can sit in the middle of a command. A second speak stops the first, and stop-speak stops it outright.

In the search bar it waits for ↩, so typing it never starts talking. A trigger can run it — | speak on every copy from an app — and so can the API.

stop-speakstopSpeaking

Stop reading aloud

Stops what speak is reading, mid-word. It reads nothing, so it needs nothing before it — bind | stop-speak to a shortcut and it works whatever is on the clipboard:

| stop-speak

Nothing being read is no mistake: it simply has nothing to stop.

openopen

Open it in an app

Opens the value where it belongs, the way ⌘O opens a row in the overlay — or, with an app's name after it, in that app:

$1 | open
$1 | open Firefox
$3 | resize 50% | open Preview
@links[2] | open com.google.Chrome

A link opens in the browser, an address in the mail app, a place in Maps, an invitation in Calendar, and files copied in Finder in their own apps. Anything else — text, JSON, code, a picture — is written to a file of its own, as Paste as File writes one, and opened in the app for that kind. The file is a copy: changing it changes nothing in the history.

The app is named as Finder shows it, case aside — firefox, Visual Studio Code — or by its bundle ID or its path. It need not be one that claims the kind of file, as with open -a in Terminal.

It pastes nothing and leaves the clipboard as it is. The value goes on unchanged, so it can sit in the middle of a command. In the search bar it waits for ↩. A trigger can run it — open every link copied in Slack in Firefox.

The API and the Shortcuts app can run it too, for web, mail and Maps links and documents only, in an app that opens that kind: never an app, a script, a workflow or another kind of link. An assistant over MCP cannot run it at all, since the text it reads may be asking it to.

tagtag

Tag it

Passes the text through exactly as it was given, and files the entry it is kept in under the tags you name:

$1 | tag #work
$3 | tag #invoice,#acme

Every tag is written with its #, and several are separated by commas, so the second line gives two. A word without its # is refused. Tags are folded to lowercase, so #Work and #work are one tag.

A tagged entry turns up when you search for the tag in the overlay, and ⌘F lists every tag you have and shows the entries under one. ⌘⇧T tags the highlighted row without a command. Some tags come without being asked: url, email, color, path, json, image and files, worked out from what each entry holds.

Tags go on the entry the result is kept in. When the text comes through unchanged — as it does above — that is the entry it came from, brought back to the top. After a filter that changes the text it is the new entry the result makes: $1 | trim | tag #tidy tags the trimmed copy.

A picture comes through as the picture: $1 | tag #shots tags a screenshot, and a trigger whose condition is | is-kind image can tag every one you copy. untag, describe and forget take a picture the same way.

Handed a list, it tags what is in it — the entries its items were written from — rather than the list:

@<$2, $5, $9> | tag #work

tags those three copies. Only an item written as a reference and nothing else counts: milk or $2 and $3 is new text, and a variable cannot be tagged, so those are passed over. Filters that keep or reorder items — sort, filter, take, reverse — keep track of where each came from; ones that make new items, like map, do not. A list with no such item in it is tagged the way text is. ⌘⌥T in the overlay writes the whole command for you: pick the rows, press ⇥, and type the tag.

To tag from a trigger, make a pipeline action that names the tag, and have the trigger run it. A trigger names actions without arguments, so the pipeline is where the tag is written down:

Keyword         tag-ticket
Pipeline        | tag #ticket

Nothing is tagged when nothing is kept: a run that is forgetten, or one whose result is sent to a variable with >, has no entry in the history to file. An abbreviation types its expansion rather than keeping it, so a tag in one does nothing either.

Filing is what the run is for, so the result goes on the clipboard without being pasted, as if nopaste followed: $3 | tag #work files $3 and leaves the app you were in alone.

tag-alltagAll

Tag everything that matches

Files every entry in the history whose text matches a pattern under the tags you name. It reads nothing it is handed, so a command starts with it:

| tag-all invoice #work
| tag-all "^https://github\.com/" #code,#review
| tag-all "(?i)\bINV-\d+" #invoice

The first word is the pattern — a regular expression, in quotes if it has spaces — and the rest are the tags, each with its # and separated by commas.

While you type it the rows below the bar are the entries it has found, so the pattern can be tried before anything is tagged. ↩ tags them and leaves the clipboard as it was. The preview line says how many; a pattern that matches nothing says so.

It searches what the overlay searches, so a screenshot is matched by the words read out of it. Tags only ever go on — an entry that has one keeps it — and ⌘⇧T is how one is taken off.

What it hands on is the entries it found, as a list, so | tag-all invoice #work > @invoices tags them and keeps them. Named from a trigger, through a pipeline action, it tags and passes the copy through untouched.

untaguntag

Take a tag off it

tag's undoing. Passes the text through exactly as it was given, and takes the tags you name off the entry it is kept in:

$1 | untag #work
$3 | untag #draft,#todo
$1 | untag

With nothing after it — the last line — it takes off every tag the entry was given.

Handed a list, it takes the tags off what is in it — the entries its items were written from — the way tag gives them: @<$2, $5> | untag #work.

It goes where tag goes — the entry the result lands in — and works from a trigger the same way, through a pipeline action such as | untag #inbox. Named with tag in one command, the later of the two wins: | tag #x | untag #x leaves x off.

A tag that comes from what an entry holds — url on a link, image on a picture — is not one it was given, so there is nothing to take off.

Like tag, it puts the result on the clipboard without pasting it.

untag-alluntagAll

Take a tag off everything that matches

tag-all's undoing. Takes the tags you name off every entry in the history whose text matches a pattern:

| untag-all invoice #work
| untag-all . #draft
| untag-all invoice
| untag-all --clear-all-tags

The second line matches everything, which is how a tag is retired altogether. With a pattern and no tags — the third — every tag comes off what the pattern matches. --clear-all-tags takes every tag off the whole history; it is spelled out rather than left to a bare untag-all, which is refused.

The rows below the bar preview it: while you type, they are the entries that match and have one of the tags — or any tag, when none is named — the ones ↩ will change. ↩ takes the tags off and leaves the clipboard as it was.

A tag that comes from what an entry holds, like url, cannot be taken off, and a pattern that finds only those says so.

describedescribe

Describe it

Passes the text through exactly as it was given, and gives the entry it is kept in a description — a few words saying what it is:

$1 | describe Invoice for Acme
$3 | describe "Staging API key"
$1 | describe

Everything after describe is the description, so quotes are only needed to keep a | or a > in it. An entry has one description: describing it again replaces the one it had, and a bare describe — the last line — takes it away.

Handed a list, it describes what is in it — each entry its items were written from, with the same words — the way tag tags them: @<$2, $5> | describe Invoice links.

The description is shown under the row in the overlay, and searching finds it. A row whose text matches a search always comes before one only its description matches, so describing things never pushes what you copied down the list.

It goes where tag goes — the entry the result is kept in — and a trigger reaches it the same way, through a pipeline action that writes the description down:

Keyword         describe-ticket
Pipeline        | describe Jira ticket

Some copies are described for you once the triggers have run, if nothing has described them: a colour by its name (#1e90ff is dodger blue), an emoji or symbol by its Unicode name, a timestamp by its date, a UUID, JWT or hash by what sort it is, a cron line in words, a snippet by its language (SQL query), prose in a language you don't read by that language, and a picture by what is in it. A trigger that describes a copy its own way wins.

Like tag, it puts the result on the clipboard without pasting it: describing a row is not using it.

allow-syncallowSync

Sync it whatever the settings say

Passes the text through exactly as it was given, and lets the entry it is kept in sync to your other Macs even where this Mac's Sync settings would keep it here — under Text only, over the size limit, under None, or after Keep on This Mac:

$1 | allow-sync

It is what Allow Sync in a row's menu does, for a trigger to do as things are copied: a pipeline action whose pipeline is | allow-sync, run by a trigger on copies from Figma, sends those screenshots on while every other picture stays here. The entry carries it, so every Mac takes it. Sync still has to be on.

Like tag, it puts the result on the clipboard without pasting it.

no-syncnoSync

Keep it on this Mac

Passes the text through exactly as it was given, and keeps the entry it is kept in on this Mac: it never syncs, whatever Sync says, and if it already had, it is taken off your other Macs:

$1 | no-sync

It is what Keep on This Mac in a row's menu does, for a trigger to do as things are copied — before sync sends the copy, which by hand you seldom beat. A pipeline action whose pipeline is | no-sync, run by a trigger on copies from a password manager or a window titled Private, keeps them on the Mac they were made on. is:local lists them, and allow-sync undoes it.

To keep a copy out of the history altogether, use forget.

Like tag, it puts the result on the clipboard without pasting it.

unfmtplainText

Paste without formatting

Puts the text on the clipboard with none of the shapes it was copied in — no bold, no links, no colour, no fonts. The characters are untouched; what changes is what the app you paste into is offered.

Run it on the newest copy from the overlay or the menu bar, or name a row:

$2 | unfmt

Run by hand it is a decision about this paste: the entry keeps how it was copied, so pressing ↩ on that row later pastes it dressed as it always was.

Named from a trigger it is a decision about the copy, because a trigger fires when something is copied and there is no paste yet to decide about. The copy is stored plain and stays plain however often it is used:

Matches regex   .
Actions         unfmt

Text that was never formatted is already plain, so running it there costs nothing and does nothing.

htmlhtml

Paste as HTML

Puts the HTML of the copy on the clipboard in place of the richer shapes offered beside it, with the plain text as always. A copy from a web page carries the page's own markup and that is what goes back; a copy from a word processor carries rich text instead, which is converted.

$1 | html

It is the companion to unfmt: one narrows a copy to the text, the other to the markup. Both leave the characters exactly as they were, and both can be run by hand — for this paste — or named from a trigger, for the copy itself.

Because formatting describes one exact run of characters, it only answers for text that is still the text that was copied. $1 | upper | html has no HTML for what it produced, and says so rather than pasting the old copy's markup over the new text. A copy that was never formatted has no HTML either.

evaleval

Expand the references in the text

Reads the text it is handed as if it had been written out in quotes: every $name, $1 and $( … ) in it stands for what it names, exactly as it would inside a "…" in the bar.

That is what turns text that was stored into text that says something. A variable holding a template is a sentence with a $ in it until this runs on it:

$greeting                  Dear $name,
$greeting | eval           Dear Marion,

Everything a quoted piece of text can hold works here, including a command:

$line | eval               Sent $(| now | strftime %H:%M)

A name that stands for nothing is left as it was written — the same answer an argument gets, so a $ that was only ever a dollar sign survives. $$ is how to write one that must not expand.

Over a list it is map that runs it, an item at a time:

@letters | map eval

It may not reach itself: text that evaluates to text that evaluates would never finish, so a second eval inside one is refused rather than run.

fillfill

Fill in a template from a hash

Reads the text it is handed as a template: every {{key}} in it stands for what that key holds in the hash named after it.

$letter                    Dear {{name}}, re: {{ref}}
$letter | fill %contact    Dear Ada, re: INV-204

The template can be written out on the spot, and the hash can be one item of a bigger one:

"Hi {{name}}" | fill %people{ada}

Spaces inside the braces are ignored, so {{ name }} is {{name}}. A key holding a hash of its own is reached with dots — {{address.city}} — and a list fills in as its items, one to a line.

A key the hash does not have is left as written, so a template can be filled in stages, and a {{ that was never a placeholder survives. What a key holds goes in as it is: a {{ inside a value is not filled in again.

--ask puts up a form for the placeholders the hash does not answer, and fills them in from what you type. Tick Save as hash to keep the answers — in the hash that was named, unless you give another — so next time there is nothing to ask:

$letter | fill --ask %contact
$letter | fill --ask

With no hash named, every placeholder is asked for.

A placeholder can offer choices after a colon. The form starts on the first, ↑ and ↓ step through them, and you can still type your own. Where nobody is asked, the first is taken. A key is asked once, so {{signoff}} further on repeats the answer:

{{signoff: Regards | Thanks | Cheers}}
{{who: @people}}       a list's items
{{link: $clip}}        one of the latest copies
{{?: Hi | Hello}}      drawn at random, never asked

It asks only where somebody asked for the run: the search bar, an abbreviation, an action run from the menu bar or a shortcut. A trigger fires on a copy, and there it fills what it can and leaves the rest — unless it says --ask-in-triggers, which asks there too. The preview above the search bar never asks; it shows what is still missing.

Over a list of templates it is map that runs it:

@letters | map fill %contact

fill knows nothing about $name. eval is the one that expands those, and the two chain: $t | fill %h | eval.

copycopySelection

Copy the selection

Presses ⌘C in the app in front and starts the pipeline from what that copied — the text you have selected. It goes on top of the history like any copy, and the result of the pipeline becomes its new value, so $1 | undo brings the selection back.

| copy | upper | paste
| copy | proofread | paste

Bound to a shortcut — Run a Pipeline in Preferences → Shortcuts — that transforms whatever is selected, where it is. The triggers do not run on what copy copies.

select does the same without touching the clipboard or the history.

It only runs inside a pipeline, from a shortcut, the menu bar or the search bar — never from a trigger or the API, which have no app in front to reach. It needs Accessibility permission, as the automatic paste does.

selectreadSelection

Read the selection, clipboard untouched

Starts the pipeline from the text selected in the app in front, and leaves the clipboard exactly as it was — nothing is added to the history either:

| select | upper | paste
| select | summarize > $notes

It asks the app for its selection directly where the app says; where it does not — most browsers — it presses ⌘C and puts back what the clipboard held straight after.

A pipeline that starts with select keeps its result off the clipboard too: it goes where the pipeline sends it — a paste, a > $name — and nowhere else.

It only runs inside a pipeline, from a shortcut, the menu bar or the search bar, and needs Accessibility permission.

pastepasteText

Paste it into the app in front

Pastes the text it is handed into the app in front — over the selection, if there is one — and passes it on unchanged, so a pipeline can go on after it:

| select | upper | paste
| select | upper | paste > $shouted

The second pastes the result and keeps it in $shouted.

It pastes whatever the Automatically paste setting says: naming it is asking for the paste. When the clipboard holds something else — after select, or a > $name — the text goes on for the paste and what was there comes back after.

With --copy it keeps what it pastes as well: the text goes on top of the history and stays on the clipboard, as though it had been copied. copypaste is that, by name.

It only runs inside a pipeline, from a shortcut, the menu bar or the search bar, and needs Accessibility permission.

copypastePipeline

Paste it, and keep it in the history

Pastes the text it is handed into the app in front, as paste does, and keeps it: it goes on top of the history and stays on the clipboard, as though it had been copied.

| select | upper | copypaste

paste after select leaves no trace — the clipboard comes back as it was. This is for when the result is worth having again: $1 is now what was pasted. It passes the text on unchanged, so a pipeline can go on after it.

It is paste --copy under a name, so it too runs from a shortcut, the menu bar or the search bar, never from a trigger or the API, and needs Accessibility permission.

Images

resizeMakes an image

Resize an image

Puts a copy of a picture on the clipboard at another size. The size is written after it, in any of four ways:

$1 | resize 50%        half the size, the same shape
$1 | resize 800x600    exactly 800 by 600
$1 | resize 800        800 wide, the height to match
$1 | resize x600       600 high, the width to match

A percentage, a width alone or a height alone keeps the picture's shape. Both sides at once is taken at its word: 800x600 of a square screenshot is 800 by 600, stretched to fit, because that is what was written.

The picture it was made from stays in the history, and the new one goes on top of it, ready to paste. It is written in the format it came in — a JPEG stays a JPEG — except a GIF, which comes out as a PNG of its first frame.

Like ocr it reads the entry's picture rather than any text, so it has to come first. What it hands on is a picture, so only another image filter can follow it — and a proportion after an earlier step is of what that step left:

$1 | resize 800x100 | resize x50      400 by 50

A size you use often can be saved as a pipeline action of your own that starts from | — | resize 50% as half — and $1 | half then does the same, anywhere resize could go.

It is typed rather than picked from a list, for the reason ocr is.

The preview says what it is about to make before ↩ — PNG image · 1600×1200 → 800×600 — and the scaling itself happens on ↩.

cropMakes an image

Crop an image

Cuts a picture down to a rectangle — the size, then how far across and down it starts:

$1 | crop 800x600+100+50    800 by 600, 100 across and 50 down
$1 | crop 800x600           800 by 600 from the middle
$1 | crop                   whatever is inside the border

On its own it takes off a border: everything round the edge that is the colour of the top left corner, which is the padding round a window screenshot or the white round a copied diagram. A picture whose edges are not all one colour has no border, and says so.

A rectangle reaching outside the picture is refused rather than cut short. It is an image filter, like resize: it comes first, or after another image filter, and only image filters can follow it — measured from what the step before left.

rotateMakes an image

Rotate an image

Turns a picture a quarter turn clockwise, or as far as it is told:

$1 | rotate          a quarter turn clockwise
$1 | rotate 180      upside down
$1 | rotate -90      a quarter turn back

Any multiple of 90 will do. An image filter, like resize.

flipMakes an image

Flip an image

Mirrors a picture — left for right, or with v top for bottom:

$1 | flip
$1 | flip v

An image filter, like resize.

pngMakes an image

Write an image as PNG

Puts a copy of a picture on the clipboard as a PNG, which keeps every pixel exactly and keeps transparency:

$1 | png

An image filter, like resize, so it can end a chain of them — $1 | crop | png.

jpegMakes an image

Write an image as JPEG

Puts a copy of a picture on the clipboard as a JPEG — far smaller than a PNG for a photo, at the cost of a little detail. The quality is out of 100, and 90 when it is not given:

$1 | jpeg
$1 | jpeg 60

A JPEG has no transparency, so anything transparent comes out white. An image filter, like resize.

fitMakes an image

Make an image fit a size

Makes a picture small enough to go where there is a limit — an attachment, an upload, a form that refuses anything over 2 MB:

$1 | fit 2MB
$1 | fit 500KB
$1 | jpeg | fit 1MB

A JPEG is compressed harder first, down to a quality of 50, since that costs no pixels; then the picture is scaled down until it fits. A PNG can only be scaled, so a screenshot that has to be small is usually better as jpeg | fit. A kilobyte is 1024 bytes, the smaller reading, so the result fits under either.

The limit is met when the picture is written, so it holds wherever it sits among the image filters, and the preview gives the largest the picture can be rather than the size it will end up. An image filter, like resize.

stripMakes an image

Strip an image's metadata

Puts a copy of a picture on the clipboard with nothing but its pixels: no location, no camera, no date it was taken. Worth doing to a photo before it goes anywhere public.

$1 | strip

Every image filter writes its picture this way, so $1 | resize 50% has no location in it either; this is the one that does nothing else. A photo stored on its side is turned the right way up first, since what said which way up it was goes with the rest.

remove-backgroundMakes an image

Remove an image's background

Keeps the subject of a picture — a person, a pet, a product on a table — and makes everything round it transparent:

$1 | remove-background
$1 | remove-background | crop

The subject is found on the Mac, and nothing is sent anywhere. The result is a PNG, since that keeps the transparency; a jpeg after it puts the subject on white. A picture with no clear subject — a screenshot of text, a plain colour — puts nothing on the clipboard. An image filter, like resize.

qrReads the image

Read a QR code or barcode

Hands on what a QR code — or a barcode — in a picture says. Screenshot one, and the link in it is text:

$1 | qr
$1 | qr | host

A picture holding several codes hands them on as a list, in the order they were found. One holding none says so.

A picture copied with a code in it is also described by what the code says — QR code: https://… — so typing part of the link into the overlay finds it without qr being run at all. This is for handing the text on.

It reads the picture, so it comes first — or right after image filters, reading what they made: $1 | crop 400x400+0+0 | qr reads only the corner. What it hands on is text, so anything can follow it.

data-uriReads the image

An image as a data URI

Hands on a picture written out as a data: URI — the whole picture as text, for putting straight into HTML, CSS or Markdown without a file to point at:

$1 | data-uri
$1 | resize 64 | png | data-uri

After image filters it writes what they made, so a small icon is a small URI. A large screenshot makes a very long one, which is why the second line above shrinks it first.

colorsReads the image

The colours in an image

Hands on the colours most of a picture is, most first, as a list of hex codes — five, or as many as it is told:

$1 | colors
$1 | colors 3
$1 | colors | first

Shades too close to one already picked are passed over, so the list is five colours rather than five greys. Transparent parts are no colour at all.

qrcodeMakes an image

Make a QR code

Makes a QR code of the text and puts it on the clipboard as a picture, ready to paste into a slide or a document. Copy a link and pick this from the menu bar, or:

$1 | qrcode
$1 | qrcode | resize 300
"https://example.com" | qrcode

It reads text, so it can follow any filter that makes some, and it makes a picture, so image filters can follow it. Each square is 10 pixels, with a margin round the code wide enough for any scanner. A QR code holds at most 2331 bytes of text.

Text

++Join

Join text on the end

Joins text onto the end of what it is given, with nothing between — a space goes in where a space is wanted.

$1 ++ "!"
$1 | ++ "!"            the same thing
$1 ++ " — " ++ $2      two clipboard items, with a dash between
$1 | trim ++ $(| strftime %F)

The words after it are the text to join, so quotes are needed only to keep the spaces at either end of it. A $( … ) runs a pipeline of its own and joins on what it produced.

Like the sums, it needs a space either side: $1 ++5 is refused rather than read as a join, so that an argument beginning with a + stays the argument it is.

trimSubstitution

Trim whitespace

Removes whitespace from the start and end of the text, leaving what is in the middle alone.

Handy at the front of a pipeline, since text copied out of a web page or a terminal usually arrives with something invisible attached:

$1 | trim | json

collapse-spacesSubstitution

Collapse runs of spaces and tabs

Replaces every run of spaces and tabs with a single space. Line breaks are left alone, so the shape of a paragraph survives while the ragged spacing of copied-out columns does not.

Pair it with trim to tidy a line completely.

squeeze-blanksSubstitution

Collapse repeated blank lines

Turns any run of two or more blank lines into a single blank line. Paragraph breaks stay; the gaps left behind by deleting things do not.

strip-htmlSubstitution

Remove HTML tags

Deletes anything between < and >, leaving the text between the tags.

This is a regex, not a parser: it is meant for pulling the words out of a snippet of markup, and it will happily eat a < that was never a tag. Do not use it to sanitise anything.

strip-ansiScript

Remove terminal colour codes

Deletes the escape codes a terminal reads as colour, bold, cursor movement and window titles, which come along when output is copied out of a log or a CI page and show up elsewhere as ^[[31m. The text between them is left alone.

$1 | strip-ansi

strip-controlScript

Delete control characters

Deletes the invisible control characters that ride along with text copied out of a terminal, a PDF or an old file — a backspace, a form feed, a stray ESC or DEL — and that break a form or a parser without ever showing. Tabs and line endings are kept.

$1 | strip-control

It takes out an ESC but not the colour code after it, which strip-ansi deletes whole. show-control shows what is there first.

show-controlScript

Show control characters

Writes every control character as the symbol that stands for it — a tab as ␉, ESC as ␛, a carriage return as ␍ — and puts a ␊ at the end of each line, so text that looks right but behaves wrongly says why.

$1 | show-control      a⇥b  →  a␉b

The lines stay where they were. strip-control deletes what this shows, apart from the tabs and line endings.

tidyPipeline

Trim, collapse spaces and blank lines

Everything that makes copied text untidy, in one go: the whitespace at either end, runs of spaces and tabs in the middle, and repeated blank lines.

This one is a pipeline action — it is nothing but three other actions in a row:

| trim | collapse-spaces | squeeze-blanks

The leading | is the text it was handed, so it can be run on the clipboard, named by a trigger, or used as a filter like any other. Everything in it is a substitution, so it is pure and previews live as you type.

sort-linesScript

Sort lines

Sorts the lines in ascending order, the way sort does — byte order, so capitals come before lowercase.

Follow it with unique-lines to get a sorted list with no repeats:

$1 | sort-lines | unique-lines

sort-naturalScript

Sort lines, numbers by their value

Sorts the lines the way a person would: a run of digits is read as a number, so item 9 comes before item 10 and v1.9 before v1.10. sort-lines sorts by byte, which puts 10 before 9.

$1 | sort-natural

Everything else is byte order, as sort-lines has it — capitals before lowercase.

unique-linesScript

Drop repeated lines

Keeps the first appearance of each line and drops the rest. Unlike uniq, the input does not have to be sorted, and the order of what survives is the order it arrived in.

reverse-linesScript

Reverse the order of the lines

Last line first. Useful for reading a log that was written oldest-first.

number-linesScript

Number the lines

Puts a right-aligned line number and two spaces in front of every line, including blank ones.

join-linesScript

Join the lines into one

Runs the lines together into a single line.

The separator is the filter's argument, and defaults to a single space. Quote it to keep a trailing space or to use something with a space in it:

$1 | join-lines ", "
$1 | join-lines ,

lfScript

Unix line endings

Ends every line with a line feed alone, as macOS and Unix write it. A Windows \r\n and an old Mac \r both become \n, so text pasted out of a Windows file stops showing a stray ^M at the end of each line.

$1 | lf

crlf and cr go the other way.

crlfScript

Windows line endings

Ends every line with \r\n, as Windows, HTTP and most network protocols want it. Lines that already end that way are left as they are, and a lone \r or \n is replaced.

$1 | crlf

crScript

Classic Mac line endings

Ends every line with a carriage return alone, as Mac OS 9 did — for the occasional old tool or file format that still expects it.

shuffleScript

Put the lines in a random order

Deals the lines out in a random order, each order as likely as any other — for picking who goes first or drawing a sample from a list.

$1 | shuffle

Typed in the search bar, the order in the preview is the one ↩ pastes. It stays the same while you go on typing.

wrapScript

Hard-wrap long lines

Breaks every line longer than a width into several, between words, so none runs past it. The width is the argument, and defaults to 72 characters:

$1 | wrap
$1 | wrap 60

Each line is wrapped on its own, and lines short enough are left alone, so a list stays a list. A word longer than the width gets a line to itself rather than being cut. To wrap text again at another width, unwrap it first:

$1 | unwrap | wrap 100

unwrapScript

Join wrapped lines into paragraphs

Joins the lines of each paragraph back into one, with a space between them — for text copied out of an email, a PDF or a terminal that broke it at a fixed width. A blank line still separates two paragraphs.

$1 | unwrap

Every line that is not blank is treated as part of a paragraph, so a list with no blank lines between its items is joined into one line too.

smart-quotesScript

Curly quotes

Turns straight quotes into curly ones: a " or ' at the start of a line, or after a space or an opening bracket, opens, and every other one closes — which makes an apostrophe the closing ’ it should be.

$1 | smart-quotes      "it's here"  →  “it’s here”

It goes by what is around each quote, so a quote straight after a dash, or an apostrophe at the start of a word ('90s), can come out facing the wrong way.

guillemetsScript

French quotes

Turns double quotes, straight or curly, into guillemets: an opening one into « and a closing one into », by the same rule smart-quotes goes by. Single quotes and apostrophes are left alone.

$1 | guillemets        il dit "oui"  →  il dit «oui»

straight-quotes turns them back.

straight-quotesScript

Straight quotes

Turns curly quotes and guillemets back into straight ones — “ ” „ ‟ « » into ", and ‘ ’ ‚ ‛ ‹ › into ' — for code, a terminal or a form that chokes on them. The space French puts inside guillemets goes with them.

$1 | straight-quotes   “it’s here”  →  "it's here"
$1 | straight-quotes   « oui »      →  "oui"

countScript

Count lines, words and characters

Replaces the text with a count of it — 12 lines, 340 words, 2,110 characters — which makes it a filter to end a pipeline with rather than one to build on.

Characters are counted as characters, not bytes, so accented letters and emoji count once each.

extract-urlsextractURLs

Every URL in the text

Every web address in the text, as a list, in the order they appear and each once — with or without its https://, since what was copied is seldom a tidy list:

$1 | extract-urls

Run by hand it puts the first on the clipboard and the rest follow a paste at a time. Email addresses are extract-emails's.

extract-emailsextractEmails

Every email address in the text

Every email address in the text, as a list, each once and as written:

$1 | extract-emails
$1 | extract-emails | join ", "

extract-datesextractDates

Every date in the text

Every date the text mentions, as a list of ISO 8601 — which every date filter reads — in the order written and each once:

$1 | extract-dates
$1 | extract-dates | map strftime "%a %e %b"

Found by the same detector Mail uses, so "next Tuesday at 3pm" counts as well as "2026-10-06". A day with no time is its midnight, and a zone the text names is kept.

extract-phonesextractPhones

Every phone number in the text

Every phone number in the text, as a list, each once and as written:

$1 | extract-phones

Found by the same detector that makes a number tappable in Mail and Messages.

extract-addressesextractAddresses

Every postal address in the text

Every postal address in the text, as a list, each once. One written over several lines is put on one, with commas between, so that it is one item:

$1 | extract-addresses

Found by the same detector that makes an address open in Maps from Mail.

replacereplaceText

Replace text, no regex

Replaces every occurrence of some text with other text, exactly as written — no regex, so nothing in either needs escaping:

$1 | replace teh the
$1 | replace "a.m." am
$1 | replace " (draft)"       nothing after it deletes it

The first argument is what to look for, and the rest what goes in its place. Quotes keep spaces in either.

truncatetruncate

Shorten to a length

Shortens text to n characters, the last of which is an ellipsis — and leaves text that is short enough alone:

$1 | truncate 80

A space just before the cut is dropped, so a cut between words does not leave word ….

languagelanguage

Say what language it is

The language the text is written in, as a code — en, fr, zh-Hans:

$1 | language

Read by macOS's own language recogniser, on any Mac. Text with too few words to tell says so rather than guessing.

isequals

Say whether it is a value

true if the text is one of the values you name, and false if it is not — what a trigger's Pipeline gives true condition ends in:

| language | is fr
| language | is fr es it        any of them
| classify work personal | is work

The text is compared whole, and exactly — fr is not FR — with the spaces and line breaks around it and around each value trimmed. So a value worked out by a command compares as you would expect: | is "$(| redact)" | ! is true when there was something to redact. matches does the same with a regex.

With a step after it, it is a gate: false stops the pipeline quietly, with nothing changed, and true hands the next step the text it was handed — not the word true. Every test does this:

| is-kind rtf | unfmt
| matches ^https?:// | remove-url-tracking

It only runs inside a pipeline. Typed after a | in the search bar, it shows what the condition would say about the clipboard.

matchesmatches

Say whether it matches a regex

true if the text matches a regex, and false if it does not — for a trigger's Pipeline gives true condition, after a step that turned the copy into something worth matching:

| headline | matches (?i)invoice
| extract-urls | matches github\.com

A pattern with a | in it has to be quoted, or the pipeline reads it as the next step: matches "^(fr|es)$".

It only runs inside a pipeline, like is.

fromcopiedFrom

Say whether it was copied in an app

true if the entry was copied in one of the apps you name, and false if it was not — for a trigger that only fires on copies from somewhere in particular:

| from Safari
| from com.apple.Safari          the same, by bundle ID
| from Safari.app                the same, as Finder names it
| from Terminal iTerm2           any of them
| from Slack && $(| matches ^https?://)
| !from Xcode

Name an app as it appears in the history, with or without .app on the end, or by its bundle identifier. Names are compared ignoring case; a bundle identifier exactly. A name with a space in it is quoted: from "Google Chrome".

It asks about the entry, not the text, so it can come anywhere in a pipeline — $3 | from Mail asks about the third entry, picture or not. An entry with no app recorded was copied in none of them.

It only runs inside a pipeline, like is.

from-window-namedinWindowNamed

Say whether it was copied in a window with this title

true if the title of the window the copy was made in matches a regex, and false if it does not. A browser puts the page's title in its window's, so this can single out a site where from can only name the whole browser. With forget after it, a trigger keeps copies from those pages out of the history:

| from-window-named login checkout      any of them
| from-window-named "bank|card"         one pattern
| from-window-named ^Inbox
| from Safari && $(| from-window-named Chase)

The regex ignores case, and is the same kind matches and from-window take. A pattern with a | in it has to be quoted, or the pipeline reads it as the next step.

In a trigger it asks about the window that was in front when the copy was made; run by hand, the window you ran it over. The title is read through Accessibility, so without that permission — and over the API, or where there is no window — it is false. The search bar waits for ↩ to ask, since a preview has no window. Scripts of your own can read the same title as $PD_WINDOW_TITLE. To ask about a copy already in the history, by the title kept with it, use from-window.

It only runs inside a pipeline, like is.

from-windowcopiedFromWindow

Say whether it was copied in a window with this title

true if the title of the window the entry was copied in matches a regex, and false if it does not. A browser puts the page's title in its window's, so this can single out a site, a document or a mail where from can only name the app:

| from-window invoice receipt        any of them
| from-window "Pull request|Issue"   one pattern
| from Safari && $(| from-window Stripe)
$3 | from-window ^Inbox

The regex ignores case. A pattern with a | in it has to be quoted, or the pipeline reads it as the next step.

It asks about the entry, by the title kept with it when it was copied, so it can come anywhere in a pipeline and answers for any row — $3 | from-window Mail asks about the third. The title is read through Accessibility, so a copy made without that permission, before titles were kept, or where there was no window has none, and is false. from-window-named asks about the window in front instead.

It only runs inside a pipeline, like is.

is-kindisKind

Say whether it is an image, files or text

true if what it is handed is one of the kinds you name, and false if it is not. There are four, and everything is exactly one of them: image, files (a copy in Finder), rtf (formatted text — RTF or HTML, as copied from a word processor or a web page) and text (text without formatting).

| is-kind image | ocr
| is-kind text rtf | trim         either of them
| !is-kind files
| is-kind rtf && from Pages

With a step after it, false stops the pipeline and true lets what it was handed through, so the first line reads the words in a picture and leaves anything else alone. It can start from an image, as from can.

It asks about the value at hand: while the text is still what was copied, that is the entry; after a step that rewrote it, it is text; after an image filter, an image.

It only runs inside a pipeline, like is.

kindpasteboardTypes

List the pasteboard types it holds

The pasteboard types of what it is handed, as a list, in the order an app pasting it reads them. A copy that kept an app's own data lists its types in the order that app offered them — Excel puts public.html first and its own com.microsoft.… types last. Anything else is richest first: a picture, the formatted shapes, then the text or the files.

$1 | kind                    public.rtf
                             public.html
                             public.utf8-plain-text
$3 | kind | first            the type an app reads first
| kind | filter com.microsoft      just Office's own

It is how to find a type to add under Ignored Types in Preferences → Copy/Paste.

Only what the history kept: a copy's text, its formatting, a picture, files and an app's own data. Markers and the previews an app draws on request are not kept, so they are not listed — copy the thing again and use From the Clipboard in Ignored Types to see those.

It asks about the value at hand, as is-kind does: while it is still what was copied, that is the entry; after a step that rewrote it, plain text; after an image filter, the type the picture will be written as. It can start from a picture or an app's object, which have no text for other filters to read.

!not

Turn true into false

false for true and true for false — for a trigger that fires on everything a test is not:

| language | is en | !
| language | !is en        the same, glued on

Glued to the front of any step, ! turns that step's answer round: !is fr, !matches ^http.

Anything but true or false is a mistake rather than a false, and says so. It only runs inside a pipeline, like is.

&&logic

True if both are true

true when the test before it and the one after it are both true:

| !is fr && is es
| language | is fr && $(| headline | matches (?i)invoice)

A step written after it — is es, !matches ^http — runs on the text the pipeline started from, not on the true or false before it. For more than one step, use a $( … ), whose leading | reads that text too.

When the left side is false the answer is already false, and the right side is not run — so put a cheap test on the left of a slow one.

An operator needs a space either side of it, and each side has to be true or false. A chain runs left to right, one step after another, like every pipeline: a || b && c is (a || b) && c. To group the other way, put the group in a $( … ).

||logic

True if either is true

true when the test before it or the one after it is true — or both:

| language | is fr || matches ^Bonjour

When the left side is true the answer is already true, and the right is not run. To accept one of several values, is fr es is shorter.

An operator needs a space either side of it, and each side has to be true or false. A chain runs left to right, one step after another, like every pipeline: a || b && c is (a || b) && c. To group the other way, put the group in a $( … ).

^^logic

True if exactly one is true

true when exactly one of the test before it and the one after it is true — either, but not both:

| matches ^http ^^ matches \.pdf$

Both sides always run, since neither decides it alone.

An operator needs a space either side of it, and each side has to be true or false. A chain runs left to right, one step after another, like every pipeline: a || b && c is (a || b) && c. To group the other way, put the group in a $( … ).

redactredact

Mask personal details and keys

The text with what it says about people, and the keys it holds, masked — before it goes into a ticket, a log or a chat with someone who should not see it:

$1 | redact
$1 | redact email phone        only those
$1 | redact secrets            only tokens and keys
$1 | redact ip                 only IP addresses
$1 | redact --ignore=ssn,iban  all but those
Jane Doe (jane@acme.example, +1 555 0100)
[name] ([email], [phone])

It masks name, place, organization, email, phone, address, card, ssn, iban, ip (IPv4 and IPv6), token (a JWT), key and private-key, all of them unless you name some; secrets is the last three. Emails, phone numbers and addresses are found by the detector Mail uses; cards, SSNs and IBANs by their shape and checksum; IP addresses by their shape, a CIDR prefix or IPv6 zone masked with them and a port left as it was; tokens by their header; API keys by the prefix AWS, GitHub, GitLab, Slack, Stripe, Anthropic, OpenAI, Google and npm give them, and private keys by their BEGIN line — those it gets right. A key with no prefix, or a password, looks like any other word and is left alone. Names, places and organisations are macOS's language tagger's best guess, and it misses some, so read the result before you share it.

Name it from a trigger to mask whatever you copy of a kind, on any Mac.

spellcheckspellcheck

Fix the spelling

The text with each word macOS would underline replaced by the spell checker's correction — and every word it has no correction for left alone:

$1 | spellcheck

It fixes spelling and nothing else: not grammar, not punctuation, and it cannot tell "their" from "there". proofread does all of that, with Apple Intelligence; this runs on any Mac, instantly, and words you taught the spell checker count as right.

loremScript

Placeholder text

Ignores the text and makes lorem ipsum instead. On its own it is the classic paragraph, word for word. Give it a number for that many fresh paragraphs, or a number and words or sentences. Typed on its own in the search bar it needs no $1 |:

lorem                 the classic paragraph
lorem 3               three paragraphs
lorem 2 sentences
lorem 20 words

What it makes always opens with Lorem ipsum dolor sit amet, and the rest is drawn at random each time — up to 1000 at once.

translatetranslate

Translate it

The text in another language, named in English or by its code:

$1 | translate french
$1 | translate to fr
$1 | translate pt-BR

It reads what language the text is in by itself. It uses macOS's own translation, on this Mac — the languages you have downloaded in System Settings → General → Language & Region → Translation Languages; one that is not downloaded says so. Text already in that language comes back as it is.

It needs macOS 26, but not Apple Intelligence. It waits for ↩ in the search bar, and is long-running: the overlay closes at once and the HUD says when the result is on the clipboard. It needs a language, so a trigger, a shortcut or the menu bar reaches it through a pipeline action of your own, | translate french saved as fr.

Markdown

md-quoteScript

Quote the text

Puts > in front of every line, which is how Markdown says a blockquote.

A blank line inside gets a bare > rather than nothing, because a blank line with no > on it ends the quote — two paragraphs quoted that way come out as one quote and one paragraph beside it. That is the whole reason this is a filter rather than something to type.

Text that is already quoted gains a second >, which is what quoting a quote looks like.

md-unquoteSubstitution

Remove one level of quoting

Takes the > off the front of every line — the way back out of a reply, a forwarded mail or a quoted message.

One level at a time, so text quoted twice needs it twice; that is what makes it usable on a reply inside a reply without flattening the lot. The space after the > goes with it where there is one, and up to three spaces in front are allowed, which is what Markdown itself allows.

$1 | md-unquote | md-unquote

md-bulletScript

Make the lines a bulleted list

Puts - in front of every line, turning a copied column or a run of sentences into a list.

Blank lines are left blank rather than given a bullet of their own, so a gap between two groups stays a gap instead of becoming an empty item.

md-numberedScript

Make the lines a numbered list

The same as md-bullet with 1., 2., 3. instead of bullets.

The count skips blank lines rather than spending a number on them, so a gap in the middle of a list does not leave a hole in the numbering.

md-checklistSubstitution

Make the lines a checklist

Turns every line into an unticked task, - [ ] , the Markdown task list that Obsidian, Bear, GitHub and most Markdown editors draw as boxes to tick.

A bullet or a number already in front of a line is replaced rather than kept, so a copied list — •, -, *, 1., 2) — becomes a checklist without doubling up. Indentation is kept, so a nested list stays nested. A line that is already a task is left as it is, ticked or not, and blank lines stay blank.

Apple Notes does not read Markdown on paste, so the boxes would arrive as - [ ] text. For Notes, paste the plain lines instead of this, select them and press ⇧⌘L.

md-codeScript

Wrap the text in a fenced code block

Puts the text inside a fenced code block. The filter's argument is the language, and is left off where there is none:

$1 | md-code swift

The fence is made longer than the longest run of backticks in the text, so pasting a snippet that already contains a fence produces a block that holds it rather than one that ends in the middle of it. Three backticks where the text has none, five where it has four. This is the part that is worth a filter: it is exactly the case where doing it by hand goes wrong, and it goes wrong quietly.

md-tableScript

Tab-separated text to a table

Turns rows of tab-separated values into a Markdown table, with the first row as the header and the | --- | row that has to follow it.

Tabs are what a copy out of a spreadsheet is separated by, which is what this is for: select a range in Numbers or Excel, copy, and paste a table into a pull request. Another separator can be named as the argument:

$1 | md-table ,

A | inside a cell is escaped, since an unescaped one would split the cell in two, and a row with fewer cells than the header is padded with empty ones — a table whose rows are different lengths does not render at all.

md-escapeSubstitution

Escape the Markdown in the text

Puts a \ in front of the characters that would otherwise be read as markup, so text survives being pasted somewhere that renders Markdown. my_file_name stays my_file_name instead of turning file into italics.

The characters escaped are \, ` `, *, _, [, ], <, > and | — the ones that mean something **anywhere in a line**. #, - and 1.` are left alone: they only mean anything at the start of a line, and escaping every hyphen in a document to catch the two that begin one is a cure worse than the problem.

md-plainPipeline

Markdown to plain text

Reads the text as Markdown and keeps only the words: no # before a heading, no ** around bold, a link's text without its address.

$1 | md-plain

It is md-html, then strip-html and html-unescape, so it reads Markdown exactly as md-html does.

md-htmlMarkdown

Markdown to HTML

Reads the text as Markdown and hands back the HTML it describes, as characters — headings, lists, quotes, fenced code, links, bold and emphasis:

# Title            <h1>Title</h1>
- one              <ul>
- two              <li>one</li>
                   <li>two</li>
                   </ul>

What comes out is a fragment rather than a whole page, since that is what goes into a template, a CMS field or an email that is built out of parts. A fence that names a language keeps it, as the language- class a highlighter looks for.

This is the source. md-rich is the same conversion put on the clipboard as formatting instead, for pasting into something that shows it rather than something that stores it.

md-richMarkdown

Paste Markdown as formatted text

Reads the text as Markdown and puts what it renders to on the clipboard as formatting — so writing in Markdown and pasting into Mail, Notes, a document or a chat window gives real headings, real bullets and real links rather than the marks that describe them.

$1 | md-rich

The value it hands on is what the rendered document reads as — the marks gone, a list reading as its bullets. So the row reads and searches as what it renders to, an app that takes only text still gets that text, and an app that can take a shape gets the shape. It is the same bargain a script that declares its output makes, and it is held to the same rule afterwards — $1 | md-rich | upper pastes plain, because upper produced characters the formatting no longer describes.

Markdown with no markup in it renders to nothing worth having, and is left exactly as it was.

Lists

splitSplit

Split into a list

Divides the text into a list on a delimiter. The delimiter is the filter's argument, and with none given it splits on line breaks:

$1 | split ,          a, b, c  →  three items
$1 | split ", "       without the spaces
$1 | split            one item a line

A list is what @name holds, so this is how one is filled from something copied — > writes over what is there, >> adds to the end of it:

$1 | split , > @recipients
$1 | split , >> @recipients

And a list on the clipboard is pasted an item at a time — the first goes on now, the next after each paste — which is what makes $1 | split , a way to deal out a row of values into a row of fields. Copying something in the middle of that ends the sequence; reaching for the same list again carries on from where it had got to.

Sent to a place in the history, a list becomes an entry per item instead — $1 | split > $2 puts each line in as a row of its own, from $2 down.

A list is text with a shape: its text is the elements, one to a line, so every other filter reads one as the lines it looks like. The shape describes that exact text and no other, so a filter that rewrites it produces text — | split is how the result becomes a list again.

joinjoin

Join a list into one string

Runs a list back together into a single piece of text, with the separator between the elements. The separator is the filter's argument and defaults to , :

@recipients | join            a, b, c
@recipients | join "; "       a; b; c
$1 | split , | join " and "

Handed plain text it reads the lines as the list, so $1 | join is a way to turn a column into a sentence without splitting it first.

A list with pictures or files from the history in it — @<$1, $2> with a screenshot at $2 — joins into one paste with each item as it was copied: text formatted, pictures inline, files attached. An app that takes only text gets the text, with the pictures left out.

A list is text with a shape: its text is the elements, one to a line, so every other filter reads one as the lines it looks like. The shape describes that exact text and no other, so a filter that rewrites it produces text — | split is how the result becomes a list again.

itemList

Take one item of a list

Takes a single element out of a list, by index. Lists count from zero: item 0 is the first, item 1 the second, and a negative index counts from the other end — item -1 is the last.

@recipients | item 1
@recipients[1]                the same item, written shorter
$1 | split , | item -1

@name[…] is this filter written on the reference and counts the same way, so @list[1] and | item 1 are one element.

Asking for an item the list does not have is an error naming the numbers it does run between, rather than an empty result that would look like an empty item. As everywhere, plain text is read as its lines, so $1 | item 2 is the third line.

first and last are this filter with the index already filled in — item 0 and item -1.

reverselistReverse

Reverse a list

Turns a list back to front: the last item first.

reverse @recipients
@recipients | reverse       the same thing
$1 | split , | reverse

Handed plain text it reads the lines as the list, the way every list filter does — so $1 | reverse is reverse-lines with a list coming out of it rather than text, which is what makes it worth having beside that one. On the clipboard that means the items are pasted from the bottom up.

An item with a line break inside it stays one item, which is the difference that matters once a list has been made: reverse-lines would turn such an item inside out.

continuelistContinue

Carry on down a list

Hands on what the clipboard has not been dealt yet of a list.

A list on the clipboard is pasted an item at a time, and copying something in the middle of that ends the sequence — which is the ordinary way to end up half way down one, since something has to be copied to fill in the field beside it. This is how the rest is asked for:

continue @recipients
@recipients | continue     the same thing

Naming the list itself always starts it again from the top. Carrying on is the rarer of the two and the one that would be surprising to get by accident, so it is the one that has to be said out loud.

What it hands over is the tail of the list, so a second continue finds where this one left off rather than starting again. A list nothing has been dealt out of, and one whose sequence ran to the end, both come back whole — there being nowhere in either to carry on from.

restartlistRestart

Start a list again from the top

Deals a list out to the clipboard again from its first item. On its own it starts the list last dealt out — the one @@ names — so a sequence that was interrupted, or ran to the end, goes round again:

restart                    the last list, from item 1
restart @recipients        a named one
@@ | restart               the same as restart on its own

Handed a list that continue or index cut short, it goes back to the top of the list that was cut, not of the piece — which is what makes restart right after a continue start from the beginning.

@@ on its own is the opposite: it carries on where the last list left off, the way continue @@ does.

history-pastehistoryPaste

Paste down the history, a row per paste

Deals the history out to the clipboard a row at a time, starting from the place you name. Each paste puts the next row on, as it was copied — a picture as the picture, formatted text formatted:

history-paste $2           $2, then $3, $4 …
$2 | history-paste         the same thing
history-paste $4 --up      $4, then $3, $2, $1
history-paste $3 5         $3 to $7, then it stops
history-paste              from $1 down

Down is newest to oldest, which is the order split > $2 puts a list in. Three copies made one after another sit at $3, $2 and $1, so history-paste $3 --up puts them back in the order they were made.

It runs until something new is copied, stop is run, or it reaches the end of the history — the rows are picked when it starts, so the history moving under it changes nothing. @@ then carries on where it stopped. Sealed rows are passed over.

stopstopList

Stop dealing a list out

Ends the list the clipboard is dealing out — a split, a gathering, a history-paste — so the next ⌘V pastes what is on the clipboard again rather than stepping on.

| stop

Where it got to is kept, so @@ carries on from there. Like pause-copying-for, it runs only when somebody starts it — a shortcut, the menu bar or the search bar — and waits for ↩.

indexlistFrom

Take a list from an item onward

Hands on a list from a given item, dropping what came before it. Lists count from zero, so index 0 is the whole list, index 2 everything from the third on, and a negative index counts from the other end — index -2 is the last two.

index @recipients 2
@recipients | index 2      the same thing
index @recipients $next    from wherever a variable says

On the clipboard that means the sequence starts there: index @rows 3 picks up a form at its fourth field. Where the answer is "wherever it left off", continue says it without the counting.

Asking for an item the list does not have is an error naming the numbers it does run between, rather than an empty list that would look like an empty answer.

maplistMap

Run a filter over every item

Runs another filter over each element of a list, and hands back a list of what each one gave. The filter to run is map's first argument, and anything after it is that filter's own:

@recipients | map upper
@recipients | map trim
| map @recipients upper       the same thing
$1 | split , | map trim

The result is always the same length as the list that went in — one answer to one item, which is the whole of what mapping means. A filter that produces a list of its own therefore lands as one item holding that list's text rather than melting into the items around it.

An operator has to be quoted, because a pipeline splits its steps on one: write map "+" 5 and map "++" "!", not map + 5.

eval is the filter this exists for. A list of templates becomes a list of what they say:

@letters | map eval

A list is text with a shape: its text is the elements, one to a line, so every other filter reads one as the lines it looks like. The shape describes that exact text and no other, so a filter that rewrites it produces text — | split is how the result becomes a list again.

firstPipeline

The first item of a list

The first element — item 0 under a name worth reaching for.

@recipients | first

A variable holds one thing, so sending a list to a $name is an error rather than a guess at which item was meant. This is one of the two answers to it — join is the other, where all of them were meant:

@recipients | first > $to

lastPipeline

The last item of a list

The last element — item -1 under a name worth reaching for.

@recipients | last

filterlistFilter

Keep the items that match a regex

Keeps the items of a list whose text matches a regex, and drops the rest:

@lines | filter ^ERROR
$1 | split , | filter @example\.com$

Each item is matched by its text — a hash by its key: value lines — and kept whole. ^ and $ are the start and end of the item, not of each line in it. Plain text is read as its lines, so $1 | filter TODO keeps the lines of a copy that mention one.

A pattern with a | in it has to be quoted, or the pipeline reads it as the next step: filter "^(ERROR|WARN)".

reject is the other half.

rejectlistReject

Drop the items that match a regex

filter the other way round: drops the items whose text matches, and keeps the rest.

@lines | reject ^DEBUG

sortlistSort

Sort a list

Sorts a list by its items' text, whole items — where sort-lines sorts lines, and would split an item with a line break in it apart.

The order is the one Finder sorts names in: file2 before file10, and capital letters not sorting ahead of everything else. Items that compare the same keep their order.

@names | sort
@names | sort | reverse       the other way

uniquelistUnique

Drop repeated items

Drops every item that repeats an earlier one, keeping the first — whole items, compared as the things they are, where unique-lines compares lines.

@emails | unique
$1 | extract-emails | unique

dedupededupe

Drop loose repeats

Drops every item that repeats an earlier one loosely — differing only in capitals, accents, punctuation or spacing — keeping the first:

Buy milk            Buy milk
buy milk!     →     Call Mom
Call  Mom
call mom

Where unique wants items to match exactly, this is for a list typed by several people. It does not guess at meaning: "Get milk" is not a repeat of "Buy milk".

sort-bylistSortBy

Sort a list of hashes by a key

Sorts a list of hashes by what one key holds, in the order sort uses:

@people | sort-by name
$1 | tsv | sort-by Due

A hash without the key sorts after every one with it. Items that are not hashes are refused rather than sorted anywhere.

flattenlistFlatten

Flatten a list

Puts the items of every list inside a list in its place, however deep — undoing the nesting map and JSON make:

@<@<a, b>, c> | flatten       a, b, c

A hash is not a list, and stays the one item it is.

takelistTake

Keep the first or last items

Keeps the first n items of a list, or with a negative n the last:

@results | take 5
@log | take -20               the last twenty

Asking for more than there are gives all of them.

droplistDrop

Drop the first or last items

Drops the first n items of a list, or with a negative n the last, and keeps the rest:

$1 | split | drop 1           without the header line
@log | drop -1

ziplistZip

Pair keys with values

Pairs the list at hand, as keys, with a list of values, into one hash:

@headers | zip @values
@<name, email> | zip $($1 | split ,)

The first key gets the first value, and so on. A key left over gets an empty value; a value left over is refused, since there is nowhere to put it. A repeated key is numbered, as csv does, so nothing is lost to it.

Case

upperScript

UPPERCASE

Every letter in capitals. ASCII only, so accented letters are left as they are.

lowerScript

lowercase

Every letter in lowercase. ASCII only, so accented letters are left as they are.

titleScript

Title Case

Capitalises the first character of every run of non-space characters and lowercases the rest, so the QUICK brown fox becomes The Quick Brown Fox. A word that mixes cases inside itself — iMac, McDonald, JavaScript — is left as it is.

For headline case, list the words to leave alone, separated by commas or spaces. Each is written exactly as listed, so small words stay lowercase and a name keeps its capitals; the first and last word of a line are still capitalised:

$1 | title a,an,and,of,the,to      The Lord of the Rings
$1 | title of NASA                 Report of NASA Findings

Spacing, tabs and line breaks are preserved exactly.

sentenceScript

Sentence case

Lowercases the text and capitalises the first letter of each sentence — after a ., ! or ?, and at the start of a line — so THE QUICK fox. IT RAN becomes The quick fox. It ran.

Names and acronyms are lowercased with everything else; it has no way to know which words should keep their capitals.

snakeScript

snake_case

Lowercases the text and joins the words with underscores. A capital inside a word is treated as the start of a new one, so myVarName becomes my_var_name, and anything that is not a letter or a digit becomes the separator.

kebabScript

kebab-case

snake with hyphens: Hello There becomes hello-there. This is also what a URL slug wants, which is what slug uses it for.

camelScript

camelCase

Joins the words, lowercase, with a capital at the start of each but the first: Hello there world becomes helloThereWorld. A capital inside a word starts a new one, as in snake, so snake and camel turn into each other.

pascalScript

PascalCase

camel with a capital on the first word too: hello there becomes HelloThere — the case of type names.

capitalize-linesScript

Capitalise each line

Gives the first letter of every line a capital and leaves the rest of the line as it is — for a list typed in a hurry. Bullets, numbers and spaces in front of the first word are stepped over:

$1 | capitalize-lines     - milk  →  - Milk

A line that starts with a digit is left alone.

alternatingScript

aLtErNaTiNg CaSe

Writes the letters lower and upper case by turns, starting lower: hello world becomes hElLo WoRlD. Anything that is not a letter is left alone and does not count towards the turns.

random-caseScript

rAndOM caSE

Writes each letter upper or lower case at random, as likely one as the other: hello world might become HeLlO wOrLD. Anything that is not a letter is left alone.

alternating is the same idea with the turns kept regular.

Encoding

base64Script

Encode as Base64

Base64-encodes the text as UTF-8, on one line with no wrapping.

base64-decode is the way back.

base64-decodeScript

Decode Base64

Decodes Base64 back to text. Whitespace in the input is ignored, so a wrapped block pasted out of a mail header still decodes.

Input that is not valid Base64 fails and leaves the text alone.

url-encodeScript

Percent-encode for a URL

Percent-encodes the text the way encodeURIComponent does, so it is safe to drop into a query string. &, =, / and ? are all escaped.

$1 | url-encode

url-decodeScript

Decode percent-encoding

Turns %20 and friends back into the characters they stand for.

md5Script

MD5 hash

The MD5 of the text as UTF-8, in lowercase hex, with no trailing newline included in what is hashed.

MD5 is broken for anything security-related. It is here for checksums and cache keys.

sha256Script

SHA-256 hash

The SHA-256 of the text as UTF-8, in lowercase hex, with no trailing newline included in what is hashed.

regex-escapeScript

Escape for a regex

Puts a \ in front of every character a regular expression would read as syntax, so the text matches itself and nothing else:

$1 | regex-escape             a.b (1)  →  a\.b \(1\)

The characters escaped are \ ^ $ . | ? * + ( ) [ ] { }, which covers the regexes in triggers, filter and replace, and those of most languages.

html-escapehtmlEscape

Escape for HTML

Writes &, <, >, " and ' as HTML entities, so the text can be pasted into a page or an attribute and read as the characters it is:

$1 | html-escape              <b> & co  →  &lt;b&gt; &amp; co

html-unescapehtmlUnescape

Read HTML entities

Reads HTML entities back as the characters they stand for — &amp;, &lt;, &hellip;, &mdash; and the others copied text is full of, and numbered ones like &#39; and &#x2014;:

$1 | html-unescape

An entity it does not know is left exactly as written.

html-numericScript

Escape as numbered HTML entities

Writes every character outside plain ASCII as a numbered entity, and &, <, >, " and ' with it — for a page or an email whose encoding cannot be trusted with é or 😀:

$1 | html-numeric             café <b>  →  caf&#233; &#60;b&#62;

html-escape writes only those five, by name. html-unescape reads either back.

rtf-sourcertfSource

The RTF source of formatted text

Writes out the RTF a formatted copy carries — the {\rtf1…} markup itself, as characters — for saving as a file, putting in a template, or seeing what a copy from Pages or Word really holds:

$1 | rtf-source

A copy from a web page carries HTML rather than RTF, and is converted. Text copied plain has no formatting to write out, and nor has text a step before rewrote — $1 | upper | rtf-source is an error, since upper produced characters the formatting no longer describes.

rtf-rich goes the other way.

rtf-richrtfRichText

Paste RTF source as formatted text

Reads the text as RTF source and puts the document it describes on the clipboard as formatting, the way md-rich does Markdown — so RTF written by hand, by a script or from a file pastes as the bold and the colours it describes:

$1 | rtf-rich

The value it hands on is what the document reads as, the markup gone, and a filter after it that rewrites that text drops the formatting. Text that does not start with {\rtf is not RTF, and is refused.

html-richhtmlRichText

Paste HTML source as formatted text

Reads the text as HTML source and puts what it describes on the clipboard as formatting, the way md-rich does Markdown and rtf-rich does RTF — so a fragment written by hand, by a script or kept in a template pastes as the bold, links and line breaks it describes:

"a <b>bold</b> word" | html-rich
$1 | html-rich

Text put into the HTML from elsewhere — a copy, say — is best escaped first, so a < in it stays a character: "<b>" ++ $(${1} | html-escape) ++ "</b>" | html-rich.

The value it hands on is what the HTML reads as, the tags gone, and a filter after it that rewrites that text drops the formatting. Text with no tag in it is not HTML, and is refused.

shell-escapeshellEscape

Quote as one shell argument

Wraps the text in single quotes, writing each ' inside it as '\'', so it can be pasted into a shell as one argument — spaces, $, *, backticks and newlines included, none of them read by the shell:

$1 | shell-escape             it's $HOME  →  'it'\''s $HOME'

The quoting is POSIX, so sh, bash and zsh all read it back as the text it was.

applescript-escapeScript

Quote as an AppleScript string

Wraps the text in double quotes and escapes what an AppleScript string needs escaped — ", \, and line breaks and tabs as \n, \r and \t — so it can be pasted into a script as one literal:

$1 | applescript-escape       say "hi"  →  "say \"hi\""

js-escapeScript

Quote as a JavaScript string

Wraps the text in single quotes, the way most JavaScript is written, and escapes ', \, line breaks, tabs and the two Unicode line separators, so it can be pasted into code as one literal:

$1 | js-escape                it's  →  'it\'s'

json-escape gives a double-quoted string, which JavaScript reads too.

swift-escapeScript

Quote as a Swift string

Wraps the text in double quotes and escapes ", \, line breaks and tabs, so it can be pasted into Swift as one literal. A \( in the text is escaped with the rest, so it stays text rather than becoming an interpolation:

$1 | swift-escape             say "hi"  →  "say \"hi\""

Math

+Arithmetic

Add a number

Adds to the number in the text.

$1 + 5
$1 | + 5      the same thing
$subtotal + $shipping

Every step of a pipeline runs in the order it is written, and a sum is an ordinary step — so there is no operator precedence here. $1 + 2 * 3 multiplies the sum by three; it does not add six. The search bar says so when a command mixes + or - with * or /, which is the only place the two readings differ.

Numbers are read and written plainly: a . for the point, no separator between the thousands, and a leading - for a negative. Text that is not a number — 12kg, $4.99, an empty clipboard — is an error rather than a guess.

-Arithmetic

Subtract a number

Subtracts from the number in the text.

$1 - 5
$1 - -5       which adds five

An operator needs a space either side of it, so that dateadd -30min and sed -E stay the arguments they are. $1 -5 is refused rather than read as a subtraction.

Every step of a pipeline runs in the order it is written, and a sum is an ordinary step — so there is no operator precedence here. $1 + 2 * 3 multiplies the sum by three; it does not add six. The search bar says so when a command mixes + or - with * or /, which is the only place the two readings differ.

Numbers are read and written plainly: a . for the point, no separator between the thousands, and a leading - for a negative. Text that is not a number — 12kg, $4.99, an empty clipboard — is an error rather than a guess.

*Arithmetic

Multiply by a number

Multiplies the number in the text.

$1 * 1.2      add 20% to a price
$1 * -1       flip the sign

Every step of a pipeline runs in the order it is written, and a sum is an ordinary step — so there is no operator precedence here. $1 + 2 * 3 multiplies the sum by three; it does not add six. The search bar says so when a command mixes + or - with * or /, which is the only place the two readings differ.

Numbers are read and written plainly: a . for the point, no separator between the thousands, and a leading - for a negative. Text that is not a number — 12kg, $4.99, an empty clipboard — is an error rather than a guess.

/Arithmetic

Divide by a number

Divides the number in the text.

$1 / 100
$total / $people

Division is the one sum that cannot always be written down exactly, so it is carried to 10 decimal places and rounded there. Everything else is exact: 0.1 + 0.2 is 0.3, not 0.30000000000000004, because the arithmetic is decimal rather than binary.

Dividing by zero is an error and leaves the text alone.

Every step of a pipeline runs in the order it is written, and a sum is an ordinary step — so there is no operator precedence here. $1 + 2 * 3 multiplies the sum by three; it does not add six. The search bar says so when a command mixes + or - with * or /, which is the only place the two readings differ.

Numbers are read and written plainly: a . for the point, no separator between the thousands, and a leading - for a negative. Text that is not a number — 12kg, $4.99, an empty clipboard — is an error rather than a guess.

hexScript

Write a number in hex

Writes a whole number in hexadecimal, lowercase, with 0x in front:

$1 | hex                 255  →  0xff

The number is read in decimal, or in hex, binary or octal when it starts 0x, 0b or 0o. Spaces, _ and , between the digits are ignored, and there is no limit on how large it can be. Anything else — a fraction, a word — fails rather than guessing.

binScript

Write a number in binary

Writes a whole number in binary, with 0b in front:

$1 | bin                 10  →  0b1010

The number is read in decimal, or in hex, binary or octal when it starts 0x, 0b or 0o. Spaces, _ and , between the digits are ignored, and there is no limit on how large it can be. Anything else — a fraction, a word — fails rather than guessing.

decScript

Write a number in decimal

Writes a hex, binary or octal number in decimal — the way back from hex and bin:

$1 | dec                 0xff  →  255

The number is read in decimal, or in hex, binary or octal when it starts 0x, 0b or 0o. Spaces, _ and , between the digits are ignored, and there is no limit on how large it can be. Anything else — a fraction, a word — fails rather than guessing.

in-wordsScript

Write a number in words

Spells a number out, the way a cheque or a contract writes it:

$1 | in-words            1234  →  one thousand two hundred thirty-four
$1 | in-words            -3.5  →  minus three point five

It writes English unless given a language code — in-words fr gives quarante-deux for 42. Spaces, _ and , between the digits are ignored; anything that is not a number fails rather than guessing.

convertScript

Convert a measurement to another unit

Converts a number and its unit into the unit named after it:

$1 | convert km          5 mi    →  8.04672 km
$1 | convert °C          98.6°F  →  37 °C
$1 | convert g           12 oz   →  340.1942775 g
$1 | convert MB          1 GiB   →  1073.741824 MB

The copy can name the unit itself — 5 miles to km, 5 mi → km — and then convert needs nothing after it.

It knows lengths, areas, volumes, masses, speeds, times, amounts of data, energy, power, pressure and temperature, spelled as symbols or words, singular or plural: ft, feet, km/h, kph, sq ft, m², fl oz, °F.

A few are read one way only. Cups, pints, quarts and gallons are US measures, and ton is the US short ton (t is the metric tonne). oz is a weight; a fluid ounce is floz. m is metres, so minutes are min. KB, MB and GB are powers of 1000 and KiB, MiB, GiB powers of 1024, and a lowercase b is still a byte — bits are bit, Mbit. C, F and K are temperatures.

A comma between groups of three digits is a thousands separator, 5,000 ft; any other comma is a decimal point, 1,5 km. The answer keeps ten significant digits; $1 | convert km | round 2 keeps two decimal places.

roundScript

Round numbers to some decimal places

Rounds every number in the text to the number of decimal places after it, or to a whole number with none, and leaves the rest of the text as it was:

$1 | round 2                 3.14159  →  3.14
$1 | round                   2.5      →  3
$1 | round 2                 8        →  8.00
$1 | convert km | round 2    5 mi     →  8.05 km

It pads with zeros, so round 2 writes money the way a price is written. A half rounds away from zero, and the rounding is done on the decimal digits, so 2.675 comes out 2.68 rather than the 2.67 binary arithmetic gives.

Thousands separators are kept, 1,234.567 becoming 1,234.6, and 1e+12 is written out in full. A number that is part of something else — a version 1.2.3, a date 2026-10-26, a time 12:30, 0x3e5 — is left alone.

sumaggregate

Add up a list of numbers

Adds up a list of numbers:

$1 | split , | sum
@prices | sum

Each item is read as a number the way the sums read one: a . for the point and nothing between the thousands. A blank item is a gap and is skipped; anything else that is not a number is an error rather than a guess.

avgaggregate

Average a list of numbers

The average of a list of numbers:

$1 | split , | avg
@prices | avg

Each item is read as a number the way the sums read one: a . for the point and nothing between the thousands. A blank item is a gap and is skipped; anything else that is not a number is an error rather than a guess.

minaggregate

The smallest of a list of numbers

The smallest of a list of numbers:

$1 | split , | min
@prices | min

Each item is read as a number the way the sums read one: a . for the point and nothing between the thousands. A blank item is a gap and is skipped; anything else that is not a number is an error rather than a guess.

maxaggregate

The largest of a list of numbers

The largest of a list of numbers:

$1 | split , | max
@prices | max

Each item is read as a number the way the sums read one: a . for the point and nothing between the thousands. A blank item is a gap and is skipped; anything else that is not a number is an error rather than a guess.

JSON

jsonScript

Pretty print JSON

Re-indents JSON with two spaces per level, leaving the keys in the order they were written.

It parses first, so it is also a way to find out whether something is valid JSON at all: if it is not, the action fails and says where. The JSON copied trigger runs this automatically for anything that parses.

Pretty printing is idempotent, and the text as it was copied stays in the entry's history, so reformatting is never lost work.

json-compactScript

Compact JSON onto one line

The opposite of json: parses the JSON and prints it again with no whitespace at all, which is what a config file or a curl body usually wants.

json-escapeScript

Quote the text as a JSON string

Wraps the text in double quotes and escapes what has to be escaped — quotes, backslashes, newlines — so the result can be pasted straight in as the value of a JSON field.

The input is treated as plain text, whatever it looks like: {"a":1} comes back as the string "{\"a\":1}".

json-hashjsonHash

Read JSON as a hash

Reads the text as JSON and hands on what it holds as a hash — an object's keys, in the order they were written — so its values can be reached by key:

$1 | json-hash > %order
%order{customer}{name}
$receipt | fill %order

An object inside becomes a hash of its own and an array a list, which may hold hashes too — %order{items} is a list, and @items[0] of it a hash. JSON that is an array at the top is a list rather than a hash.

Numbers keep the characters they were written with, so 1.50 stays 1.50. true and false are those words, and null is empty text.

Text that is not JSON is refused, and so is a bare string or number: there is nothing in one to reach by key.

to-jsontoJSON

Write it out as JSON

Writes the value at hand out as JSON, indented the way json indents: a hash as an object, with its keys in the order they were written, a list as an array, and text as a string.

%order | to-json
| to-json %order{items}
%<name: $1, tags: @tags> | to-json

It is the way back from json-hash, so what came in as a number goes out as one: text that is exactly a JSON number, true or false is written bare. Anything else is a string — 02139 is not a JSON number, and stays the text it is.

json-compact puts the result on one line.

envScript

Write a JSON object as .env lines

Writes a JSON object out as .env lines, one KEY=value to a key, in the order they were written:

{"apiKey": "abc123", "port": 8080, "db": {"host": "db.local"}}

becomes

API_KEY=abc123
PORT=8080
DB_HOST=db.local

A key is written in capitals with underscores — apiKey, api-key and api key are all API_KEY — and an object inside is flattened into its keys, joined by _. An array is kept as JSON, null is left empty, and true, false and numbers are written bare.

A value with anything but letters, digits and _ . / : @ % + , = - in it goes in double quotes, with ", \, $ and backticks escaped and a line break written \n, the way dotenv loaders read it back.

$1 | env app         every name starts APP_
%order | to-json | env

Text that is not a JSON object is refused.

Data

xmlScript

Pretty print XML

Re-indents XML with two spaces per level, one element to a line — a plist, a SOAP response, an SVG, an RSS feed.

It parses first, so it fails on XML that is not well formed and says on which line. An <?xml …?> declaration is kept when the text had one, and not added when it did not.

csvtable

Read CSV into a list of hashes

Reads comma-separated rows into a list of hashes, one per row, keyed by the first row:

$1 | csv > @rows
@rows | map fill "Hi {{Name}}, your order {{Order}} has shipped."
@rows[0]{Email}

A cell in double quotes may hold commas, line breaks and a quote written twice, as RFC 4180 has it. Blank rows are skipped.

Every row gets every column: a short one's missing cells are empty, and a longer one's extra cells are kept under column 4 and so on. A blank header is column N too, and one repeated is numbered — Name 2 — so no column is lost.

$1 | csv ;              semicolons, as some spreadsheets write
$1 | csv --no-header    a list of lists instead

tsvtable

Read a copied spreadsheet into a list of hashes

csv with tabs between the cells — which is what Numbers, Excel and Google Sheets put on the clipboard when a range of cells is copied. Copy the range with its header row, and each row below it becomes a hash:

$1 | tsv > @rows
@rows | map fill "{{Name}} <{{Email}}>"

Everything csv says holds here too, --no-header included.

keyshashKeys

The keys of a hash

The keys of a hash, as a list, in the order they were written:

%contact | keys         name, email, tags
| keys %order{items}[0]

Text that is key: value lines is a hash here as everywhere.

valueshashValues

The values of a hash

The values of a hash, as a list, in the order of their keys. Each is the thing it is — a value holding a list or a hash stays one:

%contact | values
@rows[0] | values | join ,

Time

nowThe time now

The time now

The time now, in this Mac's own zone, written as ISO 8601 so that everything here can read it back.

It reads nothing, which is what makes it a start rather than a step. Every command needs a source, so it is written with the leading | that means "the text at hand" — there just is not any:

| now                      2026-08-06T15:04:05-04:00
| now | strftime %H:%M     15:04
| now | strftime %F        2026-08-06
| now | dateadd +1w        a week today

Because it reads nothing, an empty clipboard history is no obstacle to it, nor is a picture sitting at the top of one.

With a zone named it answers in that zone — now UTC is utcnow, and now Asia/Tokyo is what time it is in Tokyo. Zones are named as they are for addtz.

A command that names the time twice names one time: the moment is taken once, when the command starts.

utcnowThe time now

The time now in UTC

The time now in UTC, written as ISO 8601 with the Z that says so — now in every other way.

| utcnow                   2026-08-06T19:04:05Z
| utcnow | strftime %FT%T  2026-08-06T19:04:05

This is the one to stamp anything that leaves the machine: a log line, a commit message, a ticket. now is the one to read off a clock in the room.

strftimeDate format

Reformat a date

Reads the text as a moment in time and writes it back out in the shape you ask for, by the rules of strftime(3).

What it will read: ISO 8601 (2026-08-06T15:04:05Z), a date on its own (2026-08-06), a log line's 2026-08-06 15:04:05, an email or HTTP header's Wed, 06 Aug 2026 15:04:05 +0000, a Unix time (1786000000, or milliseconds when it is thirteen digits or more), and failing all of those, a date written out in words anywhere in the text — August 6, 2026.

The format is the filter's argument, so it can be different every time:

$1 | strftime %A            Thursday
$1 | strftime "%B %e, %Y"   August  6, 2026
$1 | strftime %s            as a Unix time
$1 | strftime %FT%T%z       back to ISO 8601

Several words are one format, so the quotes above are only needed where you want the spaces kept exactly. With no argument it uses the format on the action itself, which is what a trigger uses too — triggers name actions without arguments, so a copy of this action with its own format is how dates get normalised on copy.

Times come out in the Mac's own time zone. Month and day names come out in English, so that a pipeline means the same thing on every machine it is run on.

addtzTime zone

Say which zone a date is in

Says where an unzoned date was written, leaving its clock alone. 2026-08-06 15:04:05 says three minutes past three without saying three o'clock where, so it is read as three o'clock here; addtz UTC says it was three o'clock in UTC all along.

$1 | addtz UTC
  2026-08-06 15:04:05
  → 2026-08-06T15:04:05Z

A date that already says where it is — an ISO 8601 offset, a Z, an email header's GMT, or a Unix time, which is an instant by itself — is an error rather than something to overwrite. Moving one of those to another zone is totz.

Zones can be named as UTC, as America/New_York, as PST — the coast that keeps daylight saving, not a fixed -08:00 — or as an offset like +05:30. With no zone named it uses the one on the action, and failing that the Mac's own.

This is the way to handle a server log, which is usually written in UTC and says so nowhere:

$1 | addtz UTC | totz America/New_York | strftime "%F %T %Z"
  2026-08-06 15:04:05
  → 2026-08-06 11:04:05 EDT

totzTime zone

Move a date to another zone

Moves a date to another zone: the same moment, read off another clock. Where addtz keeps the numbers and changes the moment, this keeps the moment and changes the numbers.

$1 | totz America/New_York
  2026-08-06T15:04:05Z
  → 2026-08-06T11:04:05-04:00

A date that never said which zone it was in is read as this Mac's time, as everything here reads one; addtz is how to say otherwise before converting.

Zones are named as they are for addtz, and with none named it moves the date to the zone on the action, or failing that to the Mac's own — which is how a time in a mail from someone three zones away becomes a time you can act on.

The result carries its new zone, so anything after it works in that zone: strftime writes the clock this left rather than moving it back.

dateaddDate offset

Move a date forwards or back

Moves a date by a span of time: a count and a unit, as many as you like, with an optional sign in front of the lot.

$1 | dateadd +2d       two days on
$1 | dateadd -30min    half an hour back
$1 | dateadd 1h30m     an hour and a half on
$1 | dateadd "-1mo 2d" a month and two days back

The units are s, min, h, d, w, mo and y, spelled out or abbreviated — sec, mins, hrs, days, weeks, months, yr all read as you would expect.

A lowercase m is minutes, as it is in Prometheus, systemd and Go. Months are mo: an uppercase M means months to some tools and minutes to others, and one shift key should not be the difference between a minute and forty-four thousand of them, so it is refused rather than guessed at.

Days and larger are counted on the calendar, not in seconds. A day later is this time tomorrow even where the clocks changed overnight and the day ran to 23 hours, and a month after the 31st is the last day of a month that has no 31st.

A date that said which zone it was in still says so afterwards, and one that did not still does not — moving a date is no answer to a question about zones, which is what addtz is for.

One limit worth knowing: a date carrying an offset — -04:00, which is all ISO 8601 can carry — is counted in that offset, since an offset knows nothing of when the clocks change. Across a change, +1d there is 24 hours rather than 23 or 25. Text with no zone is counted in the Mac's own, which does know.

epochDate format

A date as a Unix timestamp

The date as a Unix timestamp — seconds since 1970, UTC — for pasting into code, a log query or an API:

$1 | epoch                    2026-09-24T09:30:00Z  →  1790242200
| now | epoch

The other way needs nothing new: every date action already reads a Unix timestamp, seconds or milliseconds, so $1 | strftime %F %T turns one into a date you can read, and $1 | totz UTC into one with its zone.

Web

remove-url-trackingSubstitution

Strip URL tracking parameters

Removes the utm_*, fbclid and gclid parameters a link picks up on its way to you, and leaves the rest of the query string alone — including the ? that starts it, so ?utm_source=news&id=7 comes back as ?id=7 rather than as a query nothing can read.

It edits the text rather than parsing a URL, so it works on a paragraph with several links in it.

hostSubstitution

Keep only a URL's host

Reduces https://www.example.com:8080/a/b?c=1 to www.example.com:8080. Text that does not start with a scheme is left as it is.

url-schemeSubstitution

Keep only a URL's scheme

Reduces https://www.example.com/a to https. Text that does not start with a scheme and :// is left as it is.

url-userSubstitution

Keep only a URL's user name

Reduces ftp://alice:secret@example.com/ to alice — the name before the @, without the password. A URL with no user name gives nothing; text that does not start with a scheme is left as it is.

url-portSubstitution

Keep only a URL's port

Reduces https://example.com:8080/a to 8080. A URL that names no port gives nothing — it does not guess the scheme's usual one. Text that does not start with a scheme is left as it is.

url-pathSubstitution

Keep only a URL's path

Reduces https://example.com/docs/a.html?q=1#top to /docs/a.html — what is between the host and the query. It stays percent-encoded; follow it with url-decode to read it. Text that does not start with a scheme is left as it is.

url-querySubstitution

Keep only a URL's query

Reduces https://example.com/a?q=pasta&page=2#top to q=pasta&page=2, without the ?. A URL with no query gives nothing; text that does not start with a scheme is left as it is.

$1 | url-query | url-decode

url-fragmentSubstitution

Keep only a URL's fragment

Reduces https://example.com/guide#install to install, without the #. A URL with no fragment gives nothing; text that does not start with a scheme is left as it is.

httpsSubstitution

Upgrade http:// to https://

Rewrites the scheme of every http:// URL in the text. Nothing else changes.

hex-colorcolorSpelling

Write a colour as hex

Writes a copied colour as hex, #ff8800 — with a fourth pair when it is see-through — whatever notation it was copied in — hex, rgb(), hsl(), hwb(), lab(), lch(), oklab() or oklch():

$1 | hex-color

hex-color, rgb, hsl and oklch are the four spellings the ⌘L page shows for a colour, written the same way. A colour outside what sRGB can show is brought to the nearest one it can. A CSS colour name is read as that colour — "dark orange" | hex-color writes #ff8c00. Text that is not a colour is an error.

rgbcolorSpelling

Write a colour as rgb()

Writes a copied colour as rgb(255 136 0), whatever notation it was copied in — hex, rgb(), hsl(), hwb(), lab(), lch(), oklab() or oklch():

$1 | rgb

hex-color, rgb, hsl and oklch are the four spellings the ⌘L page shows for a colour, written the same way. A colour outside what sRGB can show is brought to the nearest one it can. A CSS colour name is read as that colour — "dark orange" | hex-color writes #ff8c00. Text that is not a colour is an error.

hslcolorSpelling

Write a colour as hsl()

Writes a copied colour as hsl(32deg 100% 50%) — in whole degrees and percents, so reading it back can land a step from where it started — whatever notation it was copied in — hex, rgb(), hsl(), hwb(), lab(), lch(), oklab() or oklch():

$1 | hsl

hex-color, rgb, hsl and oklch are the four spellings the ⌘L page shows for a colour, written the same way. A colour outside what sRGB can show is brought to the nearest one it can. A CSS colour name is read as that colour — "dark orange" | hex-color writes #ff8c00. Text that is not a colour is an error.

oklchcolorSpelling

Write a colour as oklch()

Writes a copied colour as oklch() — the perceptual notation newer CSS uses — whatever notation it was copied in — hex, rgb(), hsl(), hwb(), lab(), lch(), oklab() or oklch():

$1 | oklch

hex-color, rgb, hsl and oklch are the four spellings the ⌘L page shows for a colour, written the same way. A colour outside what sRGB can show is brought to the nearest one it can. A CSS colour name is read as that colour — "dark orange" | hex-color writes #ff8c00. Text that is not a colour is an error.

slugScript

URL slug

Lowercases the text and joins the words with hyphens, dropping punctuation — Hello, World! becomes hello-world. The same thing kebab does, under the name you reach for when you are naming a page.

Development

jwt-decodeScript

Read what a JWT says

Decodes a JSON Web Token into its header and payload, pretty printed, keys in the order written:

{
  "header": { "alg": "HS256", "typ": "JWT" },
  "payload": { "sub": "1234567890", "iat": 1516239022 }
}

A Bearer in front is ignored, so a copied header works too. The signature is not checked — that needs the key, and this is for reading a token, not for trusting one. iat and exp are Unix times; | json-hash and epoch take it from there.

mask-homeSubstitution

Hide the account name in paths

Writes a home folder as ~, so /Users/alice/Projects/app becomes ~/Projects/app — for a stack trace or a log pasted into an issue without your account name in it. Linux's /home/alice is shortened the same way, and /Users/Shared is left alone, since it is nobody's home.

$1 | mask-home

dirnameScript

The folder a path is in

Takes the last part off a path, leaving the folder it sits in: /Users/alice/notes/todo.md becomes /Users/alice/notes. Each line is read as a path of its own, so a list of paths works too.

$1 | dirname

basenameScript

The file name at the end of a path

Keeps the last part of a path: /Users/alice/notes/todo.md becomes todo.md, and a folder's path, with or without a trailing /, gives the folder's name. Each line is read as a path of its own.

no-extension takes the extension off as well:

$1 | basename | no-extension

extensionScript

A file name's extension

Keeps what comes after the last . of the file name, without the dot: report.final.pdf gives pdf, and archive.tar.gz gives gz. A name with no extension — including a hidden file like .zshrc — gives nothing. Each line is read as a path of its own.

no-extensionScript

Take the extension off a path

Removes the last extension from a path or file name and keeps the rest: ~/Desktop/photo.heic becomes ~/Desktop/photo, and archive.tar.gz becomes archive.tar. A name with no extension, or a hidden file like .zshrc, is left as it is. Each line is read as a path of its own.

expand-tildeScript

Write ~ as your home folder

Writes a ~ that starts a path as the home folder it stands for, so ~/Projects/app becomes /Users/you/Projects/app — for a field or a tool that does not understand ~. A ~ inside a word is left alone, and so is ~name, another account's home.

mask-home goes the other way.

uuidScript

A fresh UUID

Ignores the text entirely and produces a new lowercase UUID. A filter that generates rather than transforms, which makes $1 | uuid a quick way to get one onto the clipboard.

Typed in the search bar, the UUID in the preview is the one ↩ pastes. It stays the same while you go on typing, and the next time you open the bar you get a new one.

Apple Intelligence

proofreadproofread

Fix spelling and grammar

Corrects the spelling, grammar and punctuation of the text, and nothing else, with Apple Intelligence:

$1 | proofread
| proofread > $1

It keeps your wording. It is not asked to improve anything, only to fix what is wrong, and to leave formatting, code, URLs and names as they are. Space round the text — a trailing newline, an indent — is put back exactly as it was.

It runs on this Mac: nothing you proofread leaves it. That needs macOS 26 on a Mac with Apple Intelligence, switched on in System Settings; anywhere else it says which of those is missing. It takes up to 5,000 characters, and an answer takes a few seconds, so the search bar waits for ↩ rather than asking on every keystroke.

It is long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on the clipboard. A trigger can run it too. A language model can still get something wrong; the text as it was copied is behind the result, and $1 | undo brings it back.

summarizesummarize

Sum it up in a few sentences

Says what the text says in a few sentences, in the language it is written in, with Apple Intelligence:

$1 | summarize
$1 | ocr | summarize        a screenshot of a long thread

Names, numbers and dates are kept as written. It runs on this Mac — nothing leaves it — and needs macOS 26 on a Mac with Apple Intelligence switched on; anywhere else it says why not. It takes up to 10,000 characters, and waits for ↩ in the search bar, since an answer takes a few seconds.

It is long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on the clipboard. A trigger can run it too.

whenwhen

Read a date out of loose words

Reads a date out of the way people write one, with Apple Intelligence, and writes it as ISO 8601 — which is what every other date filter reads:

"next Tuesday at 3pm" | when                 2026-10-06T15:00:00-04:00
"Friday noon Pacific" | when | totz Europe/London
$1 | when | strftime "%A %e %B"

"Today", "tomorrow" and "next Tuesday" are counted from now, on this Mac's calendar. A zone the words name is kept; with none, it is this Mac's. A day with no time of day is midnight at its start. Words with no date in them are an error rather than a guess.

Spans — "two weeks ago", "in 3 days" — and holidays are read exactly: "Halloween at 8pm", "Christmas 2027", "Easter", "Thanksgiving" (the American one), "Chinese New Year", "Rosh Hashanah", "Eid al-Fitr", "Diwali" and about a hundred more names. A holiday of the Jewish or Islamic calendar is its first full day, though it starts the evening before. Everything else is read the way strftime, totz and dateadd read a date, which already understands "next Tuesday at 3pm" and "Friday noon Pacific". All of that works on any Mac. Only what none of it can read goes to Apple Intelligence, which runs on this Mac and needs macOS 26 with it switched on; anywhere else those say why not. A holiday it does not know, written with a time — "Vesak at 8pm" — is read as that time today. It reads up to 500 characters — a phrase, not a page — and waits for ↩ in the search bar.

It is long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on the clipboard. A trigger can run it too.

rewriterewrite

Rewrite it in another style

Rewrites the text in a style, keeping what it says — names, numbers, links and code stay exactly as they are:

$1 | rewrite concise
$1 | rewrite friendly
$1 | rewrite professional       formal works too
$1 | rewrite simple
$1 | proofread | rewrite concise

The space round the text — a trailing newline, an indent — is put back as it was. It takes up to 5,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar, and is long-running: the overlay closes at once and the HUD says when the result is on the clipboard. It needs a style, so a trigger, a shortcut or the menu bar reaches it through a pipeline action of your own — | rewrite concise.

askask

Do anything to it, in words

Does what you write after it to the text — for whatever the other filters do not already do:

$1 | ask "turn this into a haiku"
$1 | ask "write a one-line reply saying yes"
$1 | ask "which language is this?"

The model is small and has no internet: it is good at rewording and reading, and not to be trusted with facts, sums or code. Where a filter does the job — summarize, rewrite, extract, upper, sum — that filter is the better answer. It takes up to 8,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar, and is long-running: the overlay closes at once and the HUD says when the result is on the clipboard. It needs an instruction, so a trigger, a shortcut or the menu bar reaches it through a pipeline action of your own — | ask "turn this into a haiku".

headlineheadline

A few words saying what it is

A short title for the text — a few words saying what it is about. Handy for naming what you keep:

$1 | headline
$1 | describe $($1 | headline)    describes the entry by what it holds

(title is the filter that puts text in Title Case.) It reads up to 10,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar. It is long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on the clipboard. A trigger can run it too.

extractextract

Pull fields out into a hash

Copies the fields you name out of the text into a hash, one key for each — an email signature, an order confirmation, a job advert:

$1 | extract name email company
$1 | extract "order number" total > %order
$1 | extract name email | to-json
$letter | fill $(| extract name company)

Name the fields with spaces or commas between them, and quote one with a space in it. The keys come out in the order you named them, and one the text does not give is empty rather than guessed. What it hands on is a hash, so %name{key}, fill, to-json and keys all read it. It takes up to 8,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar, and is long-running: the overlay closes at once and the HUD says when the result is on the clipboard. It needs the fields to pull out, so a trigger, a shortcut or the menu bar reaches it through a pipeline action of your own — | extract name email.

classifyclassify

Choose one of your labels

Chooses the one label that best fits the text, out of the ones you name — and only those, so it can be relied on further down a pipeline:

$1 | classify bug feature question
$1 | classify urgent "can wait"
$1 | tag $($1 | classify work personal)

Name at least two, with spaces or commas between them; the label comes back exactly as you wrote it. It reads up to 8,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar, and is long-running: the overlay closes at once and the HUD says when the result is on the clipboard. It needs the labels, so a trigger, a shortcut or the menu bar reaches it through a pipeline action of your own — | classify bug feature question.

tabulatetabulate

Read a table out of prose

Turns something written as sentences into a table: a list with a hash for each thing the text lists.

$1 | tabulate                        it chooses the columns
$1 | tabulate who amount | to-json
$1 | tabulate who amount | map fill "{{who}}: {{amount}}"
$1 | tabulate item qty | map fill {{qty}} | sum

Name the columns to choose them, or leave them to it. Each cell is copied from the text as it is written, never worked out — "$20" stays "$20" — so a sum is left to sum, which gets the arithmetic right, and which reads plain numbers. It reads up to 5,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar. It is long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on the clipboard. A trigger can run it too.

taskstasks

List the things to do

The things to do that the text mentions — action items, to-dos, follow-ups — as a list, one short line each:

$1 | tasks
$1 | tasks | md-bullet
$1 | tasks >> @todo

Who a task is for and when it is due are kept with it. It finds real tasks well, but it always lists something: text with nothing to do in it — "we walked by the river" — still gives a line, so read the list rather than trusting that it is empty when it should be. It reads up to 8,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar. It is long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on the clipboard. A trigger can run it too.

pointspoints

List the main points

The main points the text makes, as a list — one short sentence each, the most important first. summarize as a list rather than a paragraph:

$1 | points
$1 | points | md-bullet
$1 | points | take 3

Names, numbers and dates are kept as written. It reads up to 10,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar. It is long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on the clipboard. A trigger can run it too.

replyreply

Draft a reply

A short reply to the message you copied, in its language, saying what you write after it — or, with nothing, a polite acknowledgement:

$1 | reply "yes, Thursday works"
$1 | reply "no thanks, maybe next quarter"
$1 | reply

Just the draft: no subject line and no notes about it. Read it before you send it. It reads up to 8,000 characters.

It runs on this Mac — nothing leaves it — and needs macOS 26 with Apple Intelligence switched on; anywhere else it says why not. It waits for ↩ in the search bar. It is long-running: run from the overlay, a shortcut or the menu bar, the overlay closes at once and the HUD says when the result is on the clipboard. A trigger can run it too.

describe-imagedescribeImage

Say what a picture shows

Says what a picture shows, in a few sentences — or, with a question after it, answers that:

$1 | describe-image
$1 | describe-image "what error is this?"
$1 | crop 800x400+0+0 | describe-image
$1 | describe $($1 | describe-image)

The last line gives a screenshot a description of what is in it, which searching finds. Text in the picture is read along with everything else, so it can say what a chart or a dialog means where ocr gives back the words alone.

It needs the model chosen in Preferences → Advanced, one that can see pictures — Claude, GPT, or a vision model on your own Mac — because Apple Intelligence reads only text. The picture is sent to that model's server, made no larger than 1,568 pixels on its longest side. It waits for ↩ in the search bar, and is long-running: run from a shortcut or the menu bar, the HUD says when the result is on the clipboard.

Fun

upside-downScript

uʍop ǝpᴉsdn

Turns the text over. Every letter, digit and bracket is swapped for the character that looks like it standing on its head, and the whole lot is reversed so that it reads from the other end.

Hello, World!  →  ¡plɹoM 'ollǝH

Brackets and quotes turn with everything else — a ( comes back as a ) — so a flipped line still looks like a line rather than like a mistake. Anything with no upside-down twin, emoji included, is left as it is and simply moves to the other end. Several lines are turned as one piece of text, so the last line comes out first, which is what the page would look like held the other way up.

These are ordinary Unicode characters, so the result pastes anywhere that will take an accented letter. Running it twice does not give you back what you started with — nothing turns a Ɩ back into a 1 — but the text as it was copied is still in the entry, behind this, which is the way back:

$1 | undo

spongebob-casePipeline

a SeNtEnCe

Another name for alternating: the letters lower and upper case by turns, starting lower, the way the mocking SpongeBob meme writes them.

a sentence  →  a SeNtEnCe

Anything that is not a letter is left alone and does not count towards the turns. It is a pipeline action that runs alternating and nothing else, so the two always agree.