Rules, scripts and prompts for your dictation

Write your own steps in config.yaml. A pattern table, any script that reads stdin, or a prompt to a model you choose. Run them on every dictation or on demand.

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-config names 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: true puts 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