# Authoring Tuple Triggers

You are helping author a **Tuple trigger**: a script (or set of scripts) that Tuple
runs automatically when something happens in a call — a call connects, a screen share
starts, a transcript finishes, and so on. This file is the complete reference. Read it
before creating or editing a trigger.

> This `AGENTS.md` (and the `CLAUDE.md` symlink beside it) is generated and maintained
> by the Tuple app. Don't edit it — your changes will be overwritten when the docs
> update. Put your work in trigger folders.

## Where you are

You're in a Tuple triggers directory. Depending on the build it's one of:

- `~/.tuple/triggers` (production)
- `~/.tuplestaging/triggers` (staging)
- `~/.tupledev/triggers` (development)

If you were launched **at the root** of this directory, you're creating a new trigger.
If you were launched **inside a single trigger's folder**, you're editing that trigger.

## Anatomy of a trigger

A trigger is a **folder** containing a `config.json` and one or more **event scripts**:

```
~/.tuple/triggers
├── pause-spotify/
│   ├── config.json
│   ├── call-connected          # runs when a call connects
│   └── call-ended              # runs when a call ends
└── triggers.log                # combined log output (managed by Tuple)
```

Scripts may also live loose in the root directory, but **prefer a folder per use
case** — it keeps related behavior together and lets the whole feature be removed by
deleting one folder.

### `config.json`

```json
{
  "name": "Pause Spotify",
  "description": "Pauses Spotify when a call connects and resumes when it ends.",
  "category": "Workflow",
  "platforms": ["macos"],
  "language": "bash"
}
```

| Field | Type | Notes |
|-------|------|-------|
| `name` | string | Human-readable title shown in Tuple. |
| `description` | string | One-line summary of what it does. |
| `platforms` | array | Any of `macos`, `windows`, `linux`. Use `["macos"]` here. |
| `language` | string | `bash`, `python`, `nodejs`, `ruby`, or `applescript`. |
| `category` | string | One of the categories below. Pick the best fit. |

**Categories** (use one of these exact strings for `category`):

| Category | Use it for |
|----------|------------|
| `Sidekick` | An assistant that helps you during the call (looks things up, takes notes, runs tasks). |
| `Real-Time Coach` | Live, in-call feedback or coaching. |
| `Call Summary` | Summarizing or processing the call/transcript, usually after it ends. |
| `Focus` | Reducing distractions during calls (Do Not Disturb, hiding notifications). |
| `Privacy` | Protecting privacy (hiding sensitive info, muting, clearing the screen). |
| `Recording` | Recording or capturing the call, screen, or audio. |
| `Notifications` | Sending alerts to you or elsewhere (Slack, system notifications) on call events. |
| `Workflow` | Automating a step in your workflow (pause music, open an app, set status). |
| `Novelty` | Just for fun. |

### Event scripts

- **The filename must start with the exact event name** (see the event list below).
  - ✅ `call-connected`, `screen-share-started`, `room-joined.notify-slack`
  - ❌ `callconnected` (missing hyphen), `spotify.call-connected` (event must be the
    prefix, not the suffix)
- You can have **multiple scripts for the same event** by adding a `.suffix`:
  `room-joined.notify-slack` and `room-joined.pause-music` both run on `room-joined`.
- Scripts can be **any executable** — a shell script with a shebang, or a binary. Start
  shell scripts with `#!/bin/zsh`, `#!/bin/bash`, `#!/usr/bin/env python3`, etc.

### Permissions & security

Tuple will **only** run a script that is:

1. **Owned by the current user**, and
2. **Executable**, and
3. **Not publicly (world) writable**.

Make new scripts executable:

```sh
chmod +x call-connected      # or: chmod 0755 call-connected
```

On macOS, triggers run through a separate XPC service, so they **don't inherit Tuple's
system permissions**. The first time a trigger needs a permission (e.g. Automation,
Accessibility), macOS prompts; granting it applies to all triggers thereafter.

## Event reference

Every script receives its data as `TUPLE_TRIGGER_*` environment variables.

**Always available, for every event:**

- `TUPLE_TRIGGER_CURRENT_USER_EMAIL`
- `TUPLE_TRIGGER_CURRENT_USER_FULL_NAME`

**Per-event variables:**

| Event | Variables |
|-------|-----------|
| `call-initiated` | `TUPLE_TRIGGER_CALLEE_EMAIL`, `TUPLE_TRIGGER_CALLEE_FULL_NAME` |
| `call-incoming` | `TUPLE_TRIGGER_CALLER_EMAIL`, `TUPLE_TRIGGER_CALLER_FULL_NAME` |
| `call-rejected` | `TUPLE_TRIGGER_CALLEE_EMAIL`, `TUPLE_TRIGGER_CALLEE_FULL_NAME` |
| `call-timed-out` | `TUPLE_TRIGGER_CALLEE_EMAIL`, `TUPLE_TRIGGER_CALLEE_FULL_NAME` |
| `call-connected` | _(none beyond the universal vars)_ |
| `call-ended` | `TUPLE_TRIGGER_CALL_LENGTH` |
| `call-transcription-started` | `TUPLE_TRIGGER_CALL_ID`, `TUPLE_TRIGGER_RECORDING_ID` |
| `call-transcription-complete` | `TUPLE_TRIGGER_CALL_ID`, `TUPLE_TRIGGER_RECORDING_ID` |
| `room-joined` | `TUPLE_TRIGGER_IS_SELF`, `TUPLE_TRIGGER_ROOM_NAME`, `TUPLE_TRIGGER_EMAIL`, `TUPLE_TRIGGER_FULL_NAME` |
| `room-left` | `TUPLE_TRIGGER_IS_SELF`, `TUPLE_TRIGGER_ROOM_NAME`, `TUPLE_TRIGGER_EMAIL`, `TUPLE_TRIGGER_FULL_NAME` |
| `screen-share-started` | `TUPLE_TRIGGER_IS_SELF`, `TUPLE_TRIGGER_EMAIL`, `TUPLE_TRIGGER_FULL_NAME` |
| `screen-share-ended` | `TUPLE_TRIGGER_IS_SELF`, `TUPLE_TRIGGER_EMAIL`, `TUPLE_TRIGGER_FULL_NAME` |
| `webcam-share-started` | `TUPLE_TRIGGER_IS_SELF`, `TUPLE_TRIGGER_EMAIL`, `TUPLE_TRIGGER_FULL_NAME` |
| `webcam-share-ended` | `TUPLE_TRIGGER_IS_SELF`, `TUPLE_TRIGGER_EMAIL`, `TUPLE_TRIGGER_FULL_NAME` |
| `participant-joined` | `TUPLE_TRIGGER_EMAIL`, `TUPLE_TRIGGER_FULL_NAME` |
| `participant-left` | `TUPLE_TRIGGER_EMAIL`, `TUPLE_TRIGGER_FULL_NAME` |

`TUPLE_TRIGGER_IS_SELF` is `true` when the event refers to the current user (e.g. *you*
started sharing), `false` for a teammate. Use it to avoid reacting to your own actions.

`TUPLE_TRIGGER_CALL_ID` names the call and `TUPLE_TRIGGER_RECORDING_ID` the exact
recording session — transcription can be stopped and restarted within one call, and
each session fires its own started/complete pair. Read the transcript back with the
`tuple` CLI: `tuple transcription show --recording $TUPLE_TRIGGER_RECORDING_ID` for
the one session, or `tuple transcription show $TUPLE_TRIGGER_CALL_ID` for the whole
call. A session with no transcript yields no records, so a summarizer script can
simply exit when there's nothing to read. The transcription events fire for every
recording session, including empty ones.

## A complete example

`~/.tuple/triggers/call-announcer/call-incoming`:

```sh
#!/bin/sh

PERSON_CALLING=$TUPLE_TRIGGER_CALLER_FULL_NAME
say "${PERSON_CALLING} would like to pair with you"
```

```sh
chmod +x ~/.tuple/triggers/call-announcer/call-incoming
```

## Testing & debugging

- **Run the script directly** to test it without making a real call. Set the
  `TUPLE_TRIGGER_*` variables your event provides (see the table above) and execute it
  from its folder:

  ```sh
  TUPLE_TRIGGER_CALLER_FULL_NAME="Ada Lovelace" ./call-incoming
  ```

  This is the fastest way to iterate — you're already in a terminal.
- **Logs** go to `triggers.log` in the triggers directory — all output from the trigger
  runner plus your scripts' stdout/stderr. Watch it while a real call exercises the
  trigger:

  ```sh
  tail -f triggers.log
  ```

  A typical entry:

  ```
  [2023-10-25T12:19:11.447] Got trigger 'screen-share-started', searching for matching user scripts
  [2023-10-25T12:19:11.448] Found 2 matching scripts
  [2023-10-25T12:19:11.453] Finished 'screen-share-started' with exit code: 0
  [2023-10-25T12:19:11.458] Finished 'clean-desktop/screen-share-started' with exit code: 127
  ```

  Exit code `0` is success; non-zero indicates an error (e.g. `127` = command not found).

## Best practices

- **One folder per use case.** Group all scripts for a feature in a descriptive folder.
- **Log generously.** Print what you're doing to stdout/stderr so `triggers.log` tells
  the story when something breaks. Absolute paths and dependencies that exist in your
  interactive shell may not be on the trigger runner's `PATH` — log and use full paths.
- **Persist state sparingly.** If you must keep state between runs, write a small file
  (JSON is easy to parse) next to the trigger's scripts.
- **Don't assume your shell environment.** Triggers don't run with your full login
  profile. Reference tools by absolute path or set `PATH` explicitly.

## Publishing (optional)

Triggers can be shared in Tuple's public directory via the
[tupleapp/community-triggers](https://github.com/tupleapp/community-triggers) repo. A
submission needs `config.json`, a `README.md`, and a 512×512 `assets/icon.png`; open a
PR against the `triggers/` folder and the Tuple team reviews it. (The `AGENTS.md` /
`CLAUDE.md` files in this directory are local helpers — don't include them in a
submission.)

---

### Checklist before you finish

- [ ] Trigger lives in its own folder with a `config.json`.
- [ ] Each script's filename starts with a valid event name from the table above.
- [ ] Every script is executable (`chmod +x`) and not world-writable.
- [ ] Scripts read their inputs from `TUPLE_TRIGGER_*` env vars.
- [ ] You logged enough to debug via `triggers.log`.
- [ ] Tested by running the script directly with sample `TUPLE_TRIGGER_*` values.
