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, or qr, data-uri or colors reading it into text, can follow one. qrcode goes the other way: it reads text and makes a picture. Six of them, now, utcnow, tag-all, untag-all, copy and select, read nothing handed to them, which is what makes them somewhere to start: | now | strftime %H:%M works on an empty history.
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. 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, tag, untag, describe, unfmt and html change no characters, so they can sit anywhere in a chain. And md-rich is the one filter here that makes formatting — it reads the text as Markdown and puts what that renders to on the clipboard, so it pastes as headings and bullets 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.
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, 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 — and from answers whether the entry was copied in one of the apps you name, | from Safari. !, &&, || and ^^ combine those answers. They are what a trigger's pipeline condition is built from — | language | is fr || $(| language | is es) — and only run inside a pipeline.
++ trim collapse-spaces squeeze-blanks strip-html tidy sort-lines unique-lines reverse-lines number-lines join-lines count extract-urls extract-emails extract-dates extract-phones extract-addresses replace truncate language is matches from ! && || ^^ redact spellcheck translate
split join item reverse continue index map first last filter reject sort unique dedupe sort-by flatten take drop zip
proofread summarize when rewrite ask headline extract classify tabulate tasks points reply
upside-down
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.
$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.
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.
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.
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.
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.
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.
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.
It only runs inside a pipeline, from a shortcut, the menu bar or the search bar, 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.
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.
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
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 ,
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, with the spaces and line breaks around it trimmed, and exactly — fr is not FR. matches does the same with a regex.
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 Terminal iTerm2 any of them
| from Slack && $(| matches ^https?://)
| !from Xcode
Name an app as it appears in the history, or by its bundle identifier. Names are compared ignoring case; 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. An entry with no app recorded was copied in none of them.
It only runs inside a pipeline, like is.
!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 names, emails and numbers
The text with what it says about people 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
Jane Doe (jane@acme.example, +1 555 0100)
[name] ([email], [phone])
It masks name, place, organization, email, phone, address and card, all of them unless you name some. Emails, phone numbers and addresses are found by the detector Mail uses, and card numbers by their shape and checksum — those it gets right. 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.
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-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-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.
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 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.
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.
Spacing, tabs and line breaks are preserved exactly. It knows nothing about which small words a style guide would leave alone.
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.
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.
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 → <b> & co
html-unescapehtmlUnescape
Read HTML entities
Reads HTML entities back as the characters they stand for — &, <, …, — and the others copied text is full of, and numbered ones like ' and —:
$1 | html-unescape
An entity it does not know is left exactly as written.
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.
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.
Data
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.
httpsSubstitution
Upgrade http:// to https://
Rewrites the scheme of every http:// URL in the text. Nothing else changes.
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
jira-linkSubstitution
Jira ticket to link
Turns a bare ticket key — ABC-123 — into a link to it.
It needs a variable called jiraBaseURL holding the root of your Jira, without a trailing slash, which you can add in the Variables pane or from the overlay with ⇧↩:
jiraBaseURL = https://jira.example.com
Until that variable exists the replacement is left as written, so you will see the ${jiraBaseURL} rather than a broken link. The Jira ticket copied trigger runs this on copy once you enable it.
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.
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