What you will get
Two scripts. slack_mentions ships with ParrotFlow and runs when you ask for it. today is one you write, and it runs on every dictation.
| You say | Script | You get |
|---|---|---|
| tell Marie and Thomas the deadline moved | slack_mentions |
tell @marie.dupont and @tleroy the deadline moved |
| status update for today’s date | today |
status update for 2026-10-10 |
today writes the date of the day you dictate. The output above is from a test run on 10 October 2026.
How a script step works
A transform with a command: body runs a program. The text arrives on stdin. Whatever the program prints on stdout replaces it. That is the whole contract.
Use a script when a replace table cannot do the job: a lookup in your own data, a date, a change of case, anything with a branch. A Python script costs one process start, about 30 ms. For a fixed phrase or a pattern, a replace table is enough and costs nothing.
Where scripts live
A transform named X owns the folder ~/.config/parrotflow/transforms/X/.
- A bare name in
command:is looked for in that folder, and nowhere else.command: today.pyon a transform namedtodayrunstransforms/today/today.py. - That folder is the working directory, so a script can open a data file beside it by its bare name.
- The script is run directly. Its first line picks the interpreter, for example
#!/usr/bin/env python3. - It needs the execute bit. Run
chmod +xon it. The docs call a missing execute bit “the likeliest thing to be wrong” with a script step. - A bare name that is not a file in the folder, like
trorsed, is looked up on yourPATH. So a one-line shell command works too.
transforms/built-in/ holds the scripts that ship. The app refreshes that folder on every launch, so do not edit files there. Copy a folder out of it into transforms/<name>/ first.
Set up the shipped Slack mentions script
-
Open
~/.config/parrotflow/transforms/slack_mentions/slack_mentions.py. The app writes it once, on first launch. After that it is yours and the app never overwrites it. -
Fill the roster. One line per person: the name as you say it, then the handle.
ROSTER = { "Marie": "@marie.dupont", "Thomas": "@tleroy", }A name matches as a whole word and only with that exact case, so “mark it as done” does not become a mention of Mark. A name already written as a handle is left alone. Never guess a handle. A wrong one pings the wrong person.
-
Leave the config entry as it is. A new install already has it:
transforms: - name: slack_mentions description: turn people's names into Slack mentions display: Slack Mentions offer: true key: s say: [slack mentions, mentions] command: slack_mentions.pyIt has no pipeline step, on purpose. A message that names someone is not always a message that should ping them. You run it when you want it:
offer: trueandkey: sput an S chip on the pill after each dictation. Press S or click the chip. It rewrites the sentence you just dictated.say:lists words for the tap-then-hold gesture. Tap the hotkey, hold it again, and say “slack mentions”. This path uses no model.description:is what “hey parrot, use Slack mentions” is matched against. That path asks a model to pick the transform, so it needs one inmodels:.
With an empty roster, the chip opens the script’s folder and the pill says open slack_mentions.py to set up Slack mentions. The text is not changed.
Check it
Run the script by hand first. It is a plain program:
cd ~/.config/parrotflow/transforms/slack_mentions
echo "tell Marie and Thomas the deadline moved" | ./slack_mentions.py
tell @marie.dupont and @tleroy the deadline moved
Then let ParrotFlow run it. Write a few cases in cases.yaml in the same folder. A case with no expect: must come back unchanged.
cases:
- input: tell Marie the deadline moved
expect: tell @marie.dupont the deadline moved
- input: Thomas and Marie are both off on Friday
expect: "@tleroy and @marie.dupont are both off on Friday"
- input: ask @marie.dupont about the invoice
- input: mark it as done
/Applications/ParrotFlow.app/Contents/MacOS/ParrotFlow --eval slack_mentions
change 2/2 = 100%
keep 2/2 = 100% <- must come back byte for byte
overall 4/4 = 100%
--eval runs the transform your config names, with the same code the app runs. Check that the spoken words reach it:
/Applications/ParrotFlow.app/Contents/MacOS/ParrotFlow --route "slack mentions" --keyed
→ slack_mentions (named outright, no model)
Write your own script
This one replaces the words “today’s date” with the date in ISO format. A replace table cannot do it, because the output changes every day.
-
Make the folder and the script:
mkdir -p ~/.config/parrotflow/transforms/todaySave this as
~/.config/parrotflow/transforms/today/today.py:#!/usr/bin/env python3 import datetime import re import sys text = sys.stdin.read() today = datetime.date.today().isoformat() text = re.sub(r"\btoday's date\b", today, text, flags=re.IGNORECASE) sys.stdout.write(text) -
Make it executable:
chmod +x ~/.config/parrotflow/transforms/today/today.py -
Add it under the
transforms:key inconfig.yaml:transforms: - name: today description: write today's date command: today.py -
Add a step at the end of
transcription.pipeline. Keep the steps that are already there. A transform with no step runs only when you ask for it.transcription: pipeline: # the steps already in your config stay here - transform: disfluency - transform: today -
Save. The app reloads the config at once.
Check it
/Applications/ParrotFlow.app/Contents/MacOS/ParrotFlow --check-config
--check-config names every transform that runs a program, every time. It also prints the full path it resolved:
· transforms: "today" runs a program — today.py
today command /Users/you/.config/parrotflow/transforms/today/today.py
Then run a sentence through the pipeline:
/Applications/ParrotFlow.app/Contents/MacOS/ParrotFlow --replace "status update for today's date"
status update for 2026-10-10
“we ship today” comes back unchanged, because it does not say “today’s date”.
Run it on demand instead
To run today only when you ask, leave out the pipeline step. Add any of these fields to the transform instead:
- name: today
description: write today's date
offer: true # a chip on the pill after each dictation
key: d # the letter on the chip
say: [todays date] # words for tap-then-hold
command: today.py
--check-config then lists the chip and its letter:
· offer 3 transform(s) on the pill, after Vocabulary
today — D
grammar — G
slack_mentions — S
--route "today's date" --keyed answers → today (named outright, no model). --replace "status update for today's date" now returns the sentence unchanged, because the step is gone.
A chip on the pill runs over the sentence you just dictated and writes the result back in place. The letter keys on the pill need the Input Monitoring permission. See Permissions.
When a script fails
Your words are never lost to a broken script. If it fails, the step keeps the text as it arrived and writes the reason to ~/Library/Logs/ParrotFlow.log. We tested three failures. The sentence came through each time, and these are the log lines:
| What went wrong | Log line |
|---|---|
| No execute bit | command: …/transforms/today/today.py is not executable — chmod +x it; kept the transcript |
| The script exited with an error | command: "broken.py" exited 1: something went wrong |
It ran longer than timeout_seconds: 1 |
command: "slow.py" took longer than 1s; kept the transcript |
--check-config also catches the missing execute bit before you dictate:
✗ transforms: "today" cannot run today.py — /Users/you/.config/parrotflow/transforms/today/today.py is not executable — chmod +x it
The docs list one more case: a script that prints nothing also leaves the text alone.
The timeout is 2 seconds unless you set timeout_seconds on the transform. It counts until the process exits. A script that goes over is stopped.
To read the log while you dictate:
tail -f ~/Library/Logs/ParrotFlow.log
Limits
- A
command:makesconfig.yamlrun code. Read any script before you add it, and do not paste a config from a source you do not trust. - Plain text in, plain text out. The script cannot see which app you dictate into. For that, use
returns: json, described in Writing a transform. - ParrotFlow pastes the text. The docs say it is not yet confirmed that a pasted
@handleturns into a real mention in Slack. Paste one into a Slack message without sending it and check. --replaceruns your pipeline steps. It does not run a transform that has no step. Use--evalfor those.
Related
- Programmable dictation, for rules, scripts and prompts.
- Text replacements for dictation, for jobs a table can do.
- ParrotFlow for developers.
- Writing a transform, for case sets and the JSON protocol.
command:in pipelines.md, the reference.