Three kinds of step
Everything you add is a transform in ~/.config/parrotflow/config.yaml. A transform has one of three bodies. Pick the cheapest one that can do the job.
| Body | What it is | Right when |
|---|---|---|
replace: |
A table of patterns and what replaces them | The answer is in the text, marked by a pattern you can write |
command: |
Any program that reads stdin and writes stdout | The rule needs code: a lookup table, casing, a branch |
prompt: |
An instruction to a language model | Only judgement will do |
Rules and scripts need no model and cost almost nothing. A prompt calls a model, so it is the slow one. Save the file and the app picks up the change at once.
Rules
A rule is a pattern and what replaces it. A source between slashes is a regular expression, and $1 writes back what it captured.
transforms:
- name: github_refs # Turn PRs into links
replace:
'[#$1](https://github.com/OWNER/REPO/pull/$1)':
['/\bPR\s*#?(\d+)\b/']
- name: priorities
replace:
'P0': ['P zero']
'P1': ['P one']
'P2': ['P two']
| You say | You get |
|---|---|
| PR 478 | [#478](https://github.com/OWNER/REPO/pull/478) |
| this is P zero | this is P0 |
An empty target deletes what matched. This fillers table removes “um”, “uh” and the comma they drag along.
- name: fillers
description: delete hesitation sounds
replace:
"": ['/[,]?\s*\b(?:mm[-‑]?hmm|uh[-‑]?huh|u+m+|u+h+|erm+|hmm+|mm+)\b[,]?/']
Scripts
A script gets the transcript on stdin and gives it back on stdout. That is the whole contract, so any language works.
transforms:
- name: slack_mentions
command: slack_mentions.py
#!/usr/bin/env python3
import re, sys
ROSTER = {"Ada": "@ada.lovelace"}
text = sys.stdin.read()
for name, handle in ROSTER.items():
# Skip a name already written as a handle. Case-sensitive, so
# "mark it as done" is not Mark.
text = re.sub(rf"(?<![@\w.]){re.escape(name)}\b", handle, text)
sys.stdout.write(text)
This one ships. Open transforms/slack_mentions/slack_mentions.py beside your config and fill the roster.
- Each transform has its own folder,
transforms/<name>/. The script runs there, so it can read its own data files by name. - The first line picks the interpreter, and the file needs the execute bit (
chmod +x). - A script that fails, prints nothing or runs past
timeout_seconds(2 by default) leaves your text exactly as it arrived. - A
command:makes your config run code.--check-confignames every one, every time.
Prompts
A prompt step is the only kind that rewrites your text with a model, and only when you add one. It runs on the model you choose.
transforms:
- name: Bug report
prompt: Turn this into a bug report.
offer: true
key: b
After you dictate, the pill shows Bug report as a button. Press B and the dictation becomes a bug report.
If the model is not running or the call fails, the text comes back exactly as it arrived. Every rewrite is written to the log with the text before and after.
Pipelines
A transform in pipeline: runs on every dictation, in the order listed. Order matters.
transcription:
pipeline:
- transform: numbers_en # "one two three" -> 123, so github_refs has digits
- transform: github_refs
A step can carry conditions. app: runs it only in some apps. when: runs it only when the text so far matches.
transcription:
pipeline:
- transform: grammar
app: /slack|outlook/ # grammar only checked in Slack and Outlook
A new install already has some shipped transforms in its pipeline, such as dates_en and numbers_en. Other shipped examples sit in transforms/built-in/ with no step, until you add one.
Run a step when you ask for it
A step that should not run on every dictation can wait to be asked. There are three ways.
transforms:
- name: slack_mentions
description: turn people's names into Slack mentions
display: Slack Mentions # what the menu bar says while it runs
offer: true # a chip on the pill after each dictation
key: s # press S to run it
say: [slack mentions, mentions] # hold the hotkey and say either one
command: slack_mentions.py
offer: trueputs a chip on the pill after each dictation.key:is the letter that runs it while the pill is up.say:lists the words you can say when you tap the hotkey, then hold it.
You can also say “hey parrot” and describe what you want. ParrotFlow matches your words against each transform’s description:.
Choose the models
Rules and scripts need no model. Prompts and spoken commands run on the models you list under models:, each under a name you pick.
models:
gemma: # on your Mac, through Ollama
api: ollama
model: gemma4:e4b-mlx
default: true # what a transform runs on when it names no model
gpt: # remote, for the harder jobs
api: openai
model: gpt-5.6-luna
api: is the protocol: ollama, openai or anthropic. A transform names its model with model: gemma. One that names none uses the default.
transforms:
- name: grammar
description: fix grammar and punctuation
model: gemma # stays on your Mac
offer: true
key: g
say: [Fix grammar]
prompt: Fix grammar and punctuation...
Check before you trust it
ParrotFlow --check-config
ParrotFlow --pipeline my-test.yaml "read config.port" --app Slack
--check-config prints what the app will actually run and names anything it had to ignore. --pipeline runs a pipeline file against a sentence, with no microphone.
You do not have to write any of this by hand. Add the ParrotFlow skill to your coding agent and describe the rule in your own words. See dictation for developers.
Read more
- Your first rule
- Spoken GitHub references
- Pipelines and Writing a transform, in the repository