Documentation

Actions

Actions are keyboard shortcuts that fire when a project is selected in Odak's search panel. They open apps, open URLs, or run scripts — with variable substitution for the project's path, git state, and custom per-project values.

All actions live in a single YAML file: ~/.odak/config.yaml. Edit it in your favourite editor — Odak watches the file and reloads automatically when you save.

The Settings → Actions tab is a read-only dashboard: it shows what's loaded, surfaces validation issues, and exposes Edit config.yaml + Reload buttons. Actions are not created, edited, or deleted from the UI — the file is the single source of truth.


Quick start #

Every action has a name, a type, and a shortcut. The shortcut fires when you've selected a project in the search panel.

actions:
  - name: Cursor
    type: app
    shortcut: cmd+c
    app: Cursor

That's the whole vocabulary. Add more blocks under actions:, save the file, and Odak reloads.

Note. YAML is the native format, but JSON configs parse too (JSON is a subset of YAML). The file ships as YAML so you can add comments explaining what each action does. Legacy JSON configs migrate to YAML automatically on first load.


Field reference #

Every field at a glance. Only name, type, and shortcut are truly always required — other requirements depend on type.

Top-level

FieldRequiredDefaultApplies toDescription
nameallDisplay name. Also the target when referenced by on_select.
typeallapp | url | script
shortcutallKeyboard combo (e.g. cmd+c). Empty string means "no binding" — use that only when the action is the target of on_select.
icontype defaultallSF Symbol override.
conditionsallGates when this action matches — see Conditional actions.

type: app

FieldRequiredDefaultDescription
app✅*Application name. Resolved via /Applications/{name}.app, bundle-ID, or ~/Applications.
app_path✅*Absolute path to a .app bundle. Use only when app can't resolve.

* Exactly one of app or app_path must be set. Prefer app — it's portable across machines.

type: url

FieldRequiredDefaultDescription
urlURL template. Supports {variables}. Opens in the default browser.

type: script

FieldRequiredDefaultDescription
scriptScript source. Supports {variables}. 10-second timeout.
languagezshzsh | bash | applescript | jxa
outputsilentsilent | notification | clipboard | hud | list
on_selectRequires output: list. Name of another action to chain with {selected} bound.

Action types #

App

Opens the selected project folder in a macOS application.

- name: VS Code
  type: app
  shortcut: cmd+v
  app: Visual Studio Code

Odak resolves app: by trying, in order:

  1. /Applications/{name}.app
  2. System applications (/System/Applications, /System/Applications/Utilities)
  3. ~/Applications/{name}.app
  4. NSWorkspace lookup by bundle identifier

If none match you'll see a validation issue in Settings. Fix it by setting app_path: instead:

- name: Custom Build
  type: app
  shortcut: cmd+shift+b
  app_path: /Users/me/MyApps/CustomBuild.app

URL

Opens a URL (with variable substitution) in your default browser.

- name: GitHub Actions
  type: url
  shortcut: cmd+shift+a
  url: "{remoteURL}/actions"

Script

Runs a shell script with the project directory as the working directory.

- name: Run Tests
  type: script
  shortcut: cmd+shift+t
  language: zsh
  output: notification
  script: |
    swift test 2>&1 | tail -20

Scripts run with a 10-second timeout and receive every resolved variable as $ODAK_* environment variables. Use language: to pick the runtime — defaults to zsh.

For multi-line scripts, use YAML's | (literal) or > (folded) block scalar — much nicer than escaped \ns:

- name: Smart push
  type: script
  shortcut: cmd+shift+u
  output: notification
  script: |
    if [ -z "$(git log origin/{branch}..HEAD 2>/dev/null)" ]; then
      echo "Nothing to push on {branch}"
    else
      git push -u origin {branch}
    fi

Variables #

Use {variable} anywhere in a url: or script: template. Every resolved variable is also exposed as a $ODAK_* environment variable inside scripts.

Project

TokenResolves to
{path}Full project path — /Users/you/code/my-app
{name}Project folder name — my-app
{dir}Parent directory path
{workspace}First .xcworkspace in the project (if any)

Git

TokenResolves to
{remoteURL}Git remote URL, normalised to HTTPS form
{branch}Current branch (via git rev-parse --abbrev-ref HEAD)
{gitHash}Short HEAD commit hash (via git rev-parse --short HEAD)

Git variables resolve to empty strings when the project isn't a git repo — the action still runs.

Context

TokenResolves to
{clipboard}Current macOS clipboard contents
{selection}Current search query in Odak
{selected}Line selected from a list-output action (see chaining)

Custom

Any key under variables: in the project's .odak file resolves as {myKey} and is injected as $ODAK_myKey.


Environment variables (scripts) #

In addition to {variable} substitution, every resolved variable is injected as $ODAK_* into the script's environment. Useful when you want to interpolate safely (no re-quoting), or when the value could contain characters that would break templated substitution.

- name: Open PR by branch
  type: script
  shortcut: cmd+shift+p
  output: silent
  script: |
    # $ODAK_BRANCH and $ODAK_REMOTEURL are safer here than {branch}/{remoteURL}
    gh pr view --web --repo "$ODAK_REMOTEURL" "$ODAK_BRANCH" \
      || gh pr create --web --repo "$ODAK_REMOTEURL" --head "$ODAK_BRANCH"

Naming: $ODAK_ + the uppercase variable name — {name}$ODAK_NAME, {remoteURL}$ODAK_REMOTEURL, custom {apiURL}$ODAK_APIURL.


Script output modes #

ModeWhat happens
silentRun in the background. No feedback. (default)
notificationmacOS notification with the first ~200 chars of stdout. On non-zero exit, shows stderr.
clipboardCopy stdout to the clipboard. On non-zero exit, copies stderr.
hudBrief on-screen HUD with the result (disappears after ~2s).
listParse stdout line-by-line into a selectable overlay. Selection can chain via on_select.

Conditional actions #

Define the same shortcut multiple times with different conditions. The first action whose conditions match wins.

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: swift test
  conditions:
    fileExists: Package.swift

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: go test ./...
  conditions:
    fileExists: go.mod

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: npm test
  conditions:
    fileExists: package.json

Condition fields

FieldTypeDescription
fileExistsstring | string[]File/directory that must exist at the project root. List form = any-of.
variablesmappingEach entry must match a .odak variable exactly.

Actions without a conditions field always match.


Interactive lists & chaining #

Set output: list to turn script stdout into a selectable list overlay. The user picks an item; if you set on_select: to the name of another action, that action runs with {selected} bound to the chosen line.

Switch branch

- name: Switch branch
  type: script
  shortcut: cmd+b
  output: list
  on_select: checkout-branch
  script: git branch --format='%(refname:short)'

- name: checkout-branch
  type: script
  shortcut: ""          # no direct binding — only runs via on_select
  output: notification
  script: git checkout {selected}

Open a pull request from a list

- name: Open PR
  type: script
  shortcut: cmd+p
  output: list
  on_select: open-pr-web
  script: gh pr list --json number,title --jq '.[] | "#\(.number) \(.title)"'

- name: open-pr-web
  type: script
  shortcut: ""
  output: silent
  script: gh pr view $(echo '{selected}' | awk '{print $1}' | tr -d '#') --web

If you omit on_select, selecting a list item copies it to the clipboard.


Per-project configuration #

Drop a .odak file at any project root to override defaults and define custom variables per-project. All fields optional.

ide: Cursor
terminal: Warp
gitGUI: Tower
variables:
  apiURL: https://staging.example.com
  team: platform

Now {apiURL} and {team} resolve in any action run on this project, and scripts also see $ODAK_APIURL and $ODAK_TEAM.

Condition matching can reference these too:

- name: Deploy
  type: script
  shortcut: cmd+d
  output: hud
  script: make deploy-{team}
  conditions:
    variables:
      team: platform

Validation #

Odak validates the config on every load and surfaces issues in Settings → Actions → Configuration Issues.

Detected problems:

A bad config does not break the previously-loaded state — Odak keeps running on the last-known-good config and shows the parse error until you fix the file.


Sharing & importing #

Any https:// URL that serves action YAML (or JSON — both parse) can be imported with one click via Odak's URL scheme. Host on a GitHub Gist, a raw repo file, or a pastebin:

odak://import?url=https://gist.githubusercontent.com/you/abc/raw/my-action.yaml

Inline JSON works too — handy for pasting into Slack/Discord where YAML's indentation would break:

odak://import?json={"name":"Deploy","type":"script","shortcut":"cmd+d","script":"make deploy"}

Imports:


Siri, Shortcuts, and Spotlight #

Odak exposes two App Intents:

Both appear in Siri, Shortcuts.app, Spotlight, and Control Center. No extra setup.


Workflows #

The per-category examples below show individual actions. This section is the opposite: each vignette is a small story about how people compose actions into a larger workflow. Pick one that fits your day and steal the pattern.

1 · The oncall responder

PagerDuty fires. You have 30 seconds to get situational awareness.

You set up each service's .odak with its observability identifiers — service name, dashboard IDs, Sentry project. Your global config.yaml uses those variables to open the right dashboards for whichever project you happen to select.

# ~/.odak/config.yaml (applies to every project with the variables set)
- name: Logs (prod)
  type: url
  shortcut: cmd+l
  url: "https://kibana.yourco.com/app/discover#/?_a=(query:(query:'service:{serviceName} AND env:prod'))"

- name: Dashboard
  type: url
  shortcut: cmd+shift+d
  url: "https://grafana.yourco.com/d/{grafanaId}?var-service={serviceName}&from=now-1h"

- name: Recent deploys
  type: url
  shortcut: cmd+shift+h
  url: "{remoteURL}/actions?query=workflow:deploy+branch:main"

- name: Oncall Slack channel
  type: url
  shortcut: cmd+shift+s
  url: "https://yourco.slack.com/channels/{oncallChannel}"
# services/payments/.odak — per-service variables
variables:
  serviceName: payments-api
  grafanaId: abc123
  oncallChannel: payments-oncall

Keystroke flow: page → Odak → type payments → select project → cmd+l (logs) → cmd+shift+d (dashboard) → cmd+shift+s (oncall channel). Four shortcuts, zero URL typing.

2 · The polyglot maintainer

You contribute to Swift packages, Go services, Node apps, and Python scripts. Memorising different test commands is mental overhead.

Bind one shortcut per action and let conditions.fileExists pick the right tool. Works the same way whether you're in Odak (Swift), api (Go), web (Node), or etl (Python).

# "Run tests" — cmd+shift+t, dispatches on project shape
- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: swift test 2>&1 | tail -30
  conditions:
    fileExists: Package.swift

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: go test ./... 2>&1 | tail -30
  conditions:
    fileExists: go.mod

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: npm test 2>&1 | tail -30
  conditions:
    fileExists: package.json

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: pytest 2>&1 | tail -30
  conditions:
    fileExists: pyproject.toml

Do the same for Build, Lint, Install deps, Open package manifest. Your hands stop caring what language the project is in.

3 · The fast reviewer

You're a reviewer on 12 PRs today. The tax of "checkout, pull, open, run tests" is the whole reason you're behind.

Turn PR list into a pickable overlay; selecting one checks it out and opens it. A second shortcut runs the tests; a third approves.

- name: Review a PR
  type: script
  shortcut: cmd+alt+r
  output: list
  on_select: checkout-and-open
  script: gh pr list --json number,title,author --jq '.[] | "#\(.number) — \(.title) (@\(.author.login))"'

- name: checkout-and-open
  type: script
  shortcut: ""
  output: notification
  script: |
    num=$(echo '{selected}' | awk '{print $1}' | tr -d '#')
    gh pr checkout "$num" && cursor .

- name: Approve current PR
  type: script
  shortcut: cmd+alt+a
  output: notification
  script: gh pr review --approve --body "LGTM ✅"

Flow: select repo → cmd+alt+r → pick PR → read → cmd+shift+t (runs the right test from workflow #2) → cmd+alt+a. One review, ~8 keypresses.

4 · The release cutter

Friday release. You bump the version, generate notes from commit history, tag, push, open the GitHub release page with the body prefilled.

- name: Cut release
  type: script
  shortcut: cmd+shift+v
  output: hud
  script: |
    set -e
    current=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
    read -p "New version (current $current): " next < /dev/tty
    notes=$(git log "$current..HEAD" --pretty=format:'- %s')
    git tag -a "$next" -m "$next"
    git push origin "$next"
    open "{remoteURL}/releases/new?tag=$next&body=$(printf %s "$notes" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read()))')"

cmd+shift+v, type the new version, and you're at GitHub's release page with every commit message already in the body. Edit and publish.

5 · The team onboarder

A new teammate joins. Instead of a 2-page "how to set up your mac" doc, you ship them one file.

Your team's dotfiles repo contains a config.yaml with every internal tool — Grafana, Sentry, Linear, internal admin panels, the staging console. On day one:

git clone [email protected]:yourco/dotfiles ~/dotfiles
ln -s ~/dotfiles/odak/config.yaml ~/.odak/config.yaml

The new teammate now has the same shortcuts as everyone else: cmd+shift+1 opens the staging admin, cmd+shift+2 opens Sentry, cmd+l opens Kibana filtered to whatever service they're on. Zero "hey what's the URL for…" questions in week one.

Per-company secrets (service identifiers, workspace slugs) live in per-project .odak files, so the shared config.yaml stays safe to commit publicly.

6 · The context switcher

Work project: Cursor + Slack + Linear. Side project: VS Code + Discord + GitHub.

Bind the same shortcut to different tools per project using conditions.variables. Each project's .odak sets a single context variable, and your config.yaml branches on it.

# ~/.odak/config.yaml
- name: IDE
  type: app
  shortcut: cmd+enter
  app: Cursor
  conditions:
    variables:
      context: work

- name: IDE
  type: app
  shortcut: cmd+enter
  app: Visual Studio Code
  conditions:
    variables:
      context: personal

- name: Chat
  type: url
  shortcut: cmd+shift+c
  url: "https://yourco.slack.com/channels/{slackChannel}"
  conditions:
    variables:
      context: work

- name: Chat
  type: url
  shortcut: cmd+shift+c
  url: "https://discord.com/channels/{discordGuild}/{discordChannel}"
  conditions:
    variables:
      context: personal
# ~/code/work-project/.odak
variables:
  context: work
  slackChannel: platform-team

# ~/code/side-project/.odak
variables:
  context: personal
  discordGuild: "123"
  discordChannel: "456"

Your fingers don't have to know which world they're in.

7 · The learn-a-new-repo ritual

You cloned a big unfamiliar repo. Before reading a line of code, you want a map.

- name: Hot files
  type: script
  shortcut: cmd+alt+1
  output: list
  on_select: open-selected-file
  script: |
    git log --pretty=format: --name-only --since='90 days ago' \
      | grep -v '^$' | sort | uniq -c | sort -rn | head -20 \
      | awk '{print $2 " (" $1 " changes)"}'

- name: Top contributors
  type: script
  shortcut: cmd+alt+2
  output: hud
  script: git shortlog -sn --no-merges | head -5

- name: Recent commits
  type: script
  shortcut: cmd+alt+3
  output: list
  on_select: show-commit
  script: git log --pretty=format:'%h %ad %s' --date=short -30

- name: show-commit
  type: script
  shortcut: ""
  output: silent
  script: |
    hash=$(echo '{selected}' | awk '{print $1}')
    open "{remoteURL}/commit/$hash"

- name: open-selected-file
  type: script
  shortcut: ""
  output: silent
  script: cursor "{path}/$(echo '{selected}' | awk '{print $1}')"

Three shortcuts tell you: which files churn the most (start there), who to ask (the top contributors), and what's happening now (recent commits). 90 seconds from clone to oriented.

8 · The speed demon

You're in a flow state. A teammate drops a GitHub issue URL in Slack. You copy it and keep moving.

- name: Open from clipboard
  type: url
  shortcut: cmd+shift+o
  url: "{clipboard}"

- name: Branch from clipboard issue
  type: script
  shortcut: cmd+shift+b
  output: notification
  script: |
    # Expects a GitHub issue URL in the clipboard
    num=$(echo "{clipboard}" | grep -oE 'issues/[0-9]+' | cut -d/ -f2)
    slug=$(gh issue view "$num" --json title --jq .title \
      | tr '[:upper:] ' '[:lower:]-' | tr -cd 'a-z0-9-' | cut -c1-40)
    git checkout -b "issue-$num-$slug"

Flow: copy the URL → select the right repo in Odak → cmd+shift+b. You're on a new branch named after the issue title, ready to code. No tabs opened, no context lost.


Examples #

Drop-in YAML you can paste into ~/.odak/config.yaml, grouped by category.

Opening apps

- name: Cursor
  type: app
  shortcut: cmd+c
  app: Cursor

- name: VS Code
  type: app
  shortcut: cmd+v
  app: Visual Studio Code

- name: Xcode
  type: app
  shortcut: cmd+x
  app: Xcode
  conditions:
    fileExists:
      - Package.swift
      - "*.xcodeproj"

- name: Terminal
  type: app
  shortcut: cmd+enter
  app: Terminal

- name: iTerm
  type: app
  shortcut: opt+enter
  app: iTerm

- name: Fork
  type: app
  shortcut: shift+enter
  app: Fork

Git & GitHub

- name: Open on GitHub
  type: url
  shortcut: cmd+g
  url: "{remoteURL}"

- name: Open branch on GitHub
  type: url
  shortcut: cmd+shift+g
  url: "{remoteURL}/tree/{branch}"

- name: Open commit
  type: url
  shortcut: cmd+shift+h
  url: "{remoteURL}/commit/{gitHash}"

- name: Open PRs
  type: url
  shortcut: cmd+shift+p
  url: "{remoteURL}/pulls"

- name: Open Actions runs
  type: url
  shortcut: cmd+shift+a
  url: "{remoteURL}/actions?query=branch:{branch}"

- name: Compare branch to main
  type: url
  shortcut: cmd+shift+c
  url: "{remoteURL}/compare/main...{branch}"

- name: Copy branch name
  type: script
  shortcut: cmd+shift+b
  output: clipboard
  script: printf "{branch}"

- name: Push current branch
  type: script
  shortcut: cmd+shift+u
  output: notification
  script: git push -u origin {branch}

- name: Pull with rebase
  type: script
  shortcut: cmd+shift+y
  output: notification
  script: git pull --rebase

Testing & linting

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: swift test 2>&1 | tail -30
  conditions:
    fileExists: Package.swift

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: go test ./... 2>&1 | tail -30
  conditions:
    fileExists: go.mod

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: npm test 2>&1 | tail -30
  conditions:
    fileExists: package.json

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: pytest 2>&1 | tail -30
  conditions:
    fileExists:
      - pytest.ini
      - pyproject.toml

- name: Test
  type: script
  shortcut: cmd+shift+t
  output: notification
  script: cargo test 2>&1 | tail -30
  conditions:
    fileExists: Cargo.toml

- name: Lint
  type: script
  shortcut: cmd+shift+l
  output: notification
  script: swiftlint --quiet 2>&1 | tail -20
  conditions:
    fileExists: .swiftlint.yml

- name: Lint
  type: script
  shortcut: cmd+shift+l
  output: notification
  script: eslint . 2>&1 | tail -20
  conditions:
    fileExists: .eslintrc.json

Build & run

- name: Build
  type: script
  shortcut: cmd+b
  output: notification
  script: xcodebuild -scheme {name} build 2>&1 | tail -20

- name: Clean build
  type: script
  shortcut: cmd+shift+k
  output: notification
  script: xcodebuild -scheme {name} clean build 2>&1 | tail -20

- name: Install deps
  type: script
  shortcut: cmd+i
  output: notification
  script: npm install
  conditions:
    fileExists: package.json

- name: Install deps
  type: script
  shortcut: cmd+i
  output: notification
  script: go mod download
  conditions:
    fileExists: go.mod

CI/CD & deploys

- name: Deploy staging
  type: script
  shortcut: cmd+shift+d
  output: hud
  script: make deploy-staging

- name: Deploy prod
  type: script
  shortcut: cmd+ctrl+d
  output: hud
  script: make deploy-prod

- name: Trigger GitHub workflow
  type: script
  shortcut: cmd+w
  output: notification
  script: gh workflow run deploy.yml --ref {branch}

Observability

Per-project variables in .odak make these reusable across services:

- name: Datadog
  type: url
  shortcut: cmd+shift+1
  url: "https://app.datadoghq.com/dashboard/{dashboardId}?tpl_var_service={serviceName}&tpl_var_env=prod"

- name: Grafana
  type: url
  shortcut: cmd+shift+2
  url: "https://grafana.yourco.com/d/{grafanaId}?var-service={serviceName}"

- name: Kibana logs
  type: url
  shortcut: cmd+l
  url: "https://kibana.yourco.com/app/discover#/?_a=(query:(query:'service:{serviceName} AND env:prod'))"

- name: Sentry
  type: url
  shortcut: cmd+shift+e
  url: "https://sentry.io/organizations/yourco/issues/?project={sentryProject}"

Team collaboration

- name: Copy PR link
  type: script
  shortcut: cmd+alt+p
  output: clipboard
  script: gh pr view --json url --jq .url | tr -d '\n'

- name: Copy Slack snippet
  type: script
  shortcut: cmd+alt+s
  output: clipboard
  script: |
    printf '%s\n[%s] %s' "{remoteURL}/tree/{branch}" "{name}" "{branch}"

- name: Search Linear
  type: url
  shortcut: cmd+alt+l
  url: "https://linear.app/your-team/search?q={selection}"

Project navigation

- name: Reveal in Finder
  type: script
  shortcut: cmd+f
  output: silent
  script: open {path}

- name: Copy project path
  type: script
  shortcut: cmd+alt+c
  output: clipboard
  script: printf '{path}'

- name: Open README
  type: script
  shortcut: cmd+r
  output: silent
  script: open README.md

- name: Project size
  type: script
  shortcut: cmd+alt+z
  output: hud
  script: du -sh {path} | awk '{print $1}'

- name: Recent files
  type: script
  shortcut: cmd+alt+r
  output: list
  script: find {path} -type f -mtime -7 -not -path '*/.*' | head -20

Interactive workflows

Scripts that output a selectable list — pair with on_select to chain the next step.

- name: Switch branch
  type: script
  shortcut: cmd+b
  output: list
  on_select: checkout-branch
  script: git branch --format='%(refname:short)'

- name: checkout-branch
  type: script
  shortcut: ""
  output: notification
  script: git checkout {selected}

- name: Run npm script
  type: script
  shortcut: cmd+alt+n
  output: list
  on_select: exec-npm-script
  script: jq -r '.scripts | keys[]' package.json
  conditions:
    fileExists: package.json

- name: exec-npm-script
  type: script
  shortcut: ""
  output: hud
  script: npm run {selected}

- name: Make target
  type: script
  shortcut: cmd+alt+m
  output: list
  on_select: exec-make
  script: grep -E '^[a-zA-Z_-]+:' Makefile | sed 's/:.*//'
  conditions:
    fileExists: Makefile

- name: exec-make
  type: script
  shortcut: ""
  output: hud
  script: make {selected}

- name: Recent commits
  type: script
  shortcut: cmd+alt+h
  output: list
  on_select: copy-commit-hash
  script: git log --pretty=format:'%h %s' -20

- name: copy-commit-hash
  type: script
  shortcut: ""
  output: clipboard
  script: printf %s "{selected}" | awk '{print $1}' | tr -d '\n'

Language-specific

Use conditions.fileExists to bind the same shortcut to the right tool per project.

- name: Open Package Manager
  type: script
  shortcut: cmd+alt+p
  output: silent
  script: open Package.swift
  conditions:
    fileExists: Package.swift

- name: Open Package Manager
  type: script
  shortcut: cmd+alt+p
  output: silent
  script: open package.json
  conditions:
    fileExists: package.json

- name: Open Package Manager
  type: script
  shortcut: cmd+alt+p
  output: silent
  script: open Cargo.toml
  conditions:
    fileExists: Cargo.toml

- name: Open Package Manager
  type: script
  shortcut: cmd+alt+p
  output: silent
  script: open go.mod
  conditions:
    fileExists: go.mod

- name: Open Package Manager
  type: script
  shortcut: cmd+alt+p
  output: silent
  script: open pyproject.toml
  conditions:
    fileExists:
      - pyproject.toml
      - requirements.txt

Schema reference #

Full action schema in YAML (every field; comments annotate requirements):

- name: string                        # required — display name + on_select target
  type: app | url | script            # required
  shortcut: cmd+c                     # required — "" = no binding (chain-only target)

  # Optional (all types)
  icon: terminal                      # SF Symbol name override
  conditions:                         # first matching action wins
    fileExists:                       # string or list of strings
      - Package.swift
      - go.mod
    variables:
      team: platform                  # all must match .odak variables

  # type: app — exactly one of `app` or `app_path`
  app: Cursor                         # name — preferred, portable
  app_path: /Applications/Cursor.app  # absolute path

  # type: url
  url: "{remoteURL}/actions"

  # type: script
  script: swift test
  language: zsh                       # zsh (default) | bash | applescript | jxa
  output: notification                # silent (default) | notification | clipboard | hud | list
  on_select: checkout-branch          # only valid with output: list

Root object:

version: 1        # optional — reserved for future migrations
actions:          # required
  - …

Shortcut syntax #

Human-readable and case-insensitive: cmd+c, opt+enter, cmd+shift+t, ctrl+space.

Modifiers: cmd, opt (alias: alt), ctrl, shift.
Keys: a–z, 0–9, return/enter, tab, space, escape, delete, left/right/up/down, f1–f19.

Reserved by macOS (never fire): cmd+q, cmd+h, cmd+m, cmd+w, cmd+tab, cmd+space.


FAQ #

How do I back up my actions? The whole config is a single file — cp ~/.odak/config.yaml ~/my-actions.yaml.bak.

Can I share one action with a teammate? Yes — paste the YAML block into Slack/GitHub and they paste it into their file, or use odak://import?json=… for one-click install.

Why aren't my changes picking up? Check Settings → Actions → Configuration Issues. A parse error blocks the reload until you fix the syntax. Odak keeps running on the last-known-good config so you can keep working while you fix the file.

Does .odak need to exist for a project? No — it's purely for overrides and custom variables. Actions work fine without it.

My shortcut doesn't fire. Three common causes: (1) it's macOS-reserved (see above); (2) another action has the same combo and its conditions match first; (3) the app window isn't focused — Odak shortcuts only fire while the search panel is active.

Why no GUI to add actions? By design. Odak is for developers; editing a single YAML file is faster than a form, portable across machines via a dotfile repo, and easier to share. The Settings panel still shows what's loaded and surfaces validation issues.