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.

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  →  &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.

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

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