# Terse Create

> Create a Terse workflow. Use when the user wants to build an automation that reacts to events (GitHub PRs, Slack messages, Linear issues, cron schedules, webhooks) and takes actions, or wants to get started with Terse. Bootstraps a new Terse project first when none exists.

- Skill: `terseai/terse-create` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add terseai/terse-create`
- Raw SKILL.md: https://api.skillmd.com/api/skills/terseai/terse-create/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: terseai (https://skillmd.com/u/terseai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/terseai/terse-create

---


<!-- Generated from templates/terse-create.md.hbs by scripts/build-skills.mjs. Edit the template, not this file. -->

# Create a Terse Workflow

Create a Terse workflow based on: **$ARGUMENTS**

A workflow is the thing being automated; in code it is defined as a job (`createJob` in `src/terse.jobs.ts`).

## Learning how to build the workflow

You should use the following sources to get context on the Terse platform:

1. The code in the project itself if it is present: (src/terse.jobs.ts, src/jobs/)
2. The generated files should be the primary source of truth and explain how Terse works: `src/terse.generated.ts` is the entry point — its header lists every available integration and carries a line-numbered index of the `src/terse.generated/` folder, which holds one file per integration and kind (`<integration>.triggers.ts` for trigger factories and payload types, `<integration>.tools.ts` for typed tool methods, resources and skills, workspace schema files like `attio.schemas.ts`, plus shared plumbing in `common.ts`). Use the index to open only the files the workflow needs. The generated files answer what exists and its exact shape; when they disagree with the live docs, the generated files win — a missing helper means rerun `terse generate`, never code against a docs claim the generated files don't back.
3. The terse cli: (terse --help)
4. The fourth and last option should be to use the live terse docs (docs.useterse.ai). Fetch first the index page (https://docs.useterse.ai/llms.txt) to discover available pages and then only pull the specific pages you need to implement the workflow.

**Do not search or read the dist files for the terse-sdk package.**

## Prefer computer use when available

Use computer-use tools whenever the environment exposes them and the task can be completed reliably through a graphical interface. This includes browser and desktop surfaces in apps such as Claude Code, ChatGPT, and other installed or signed-in applications.

- Before any computer-use call, obtain explicit permission, including for read-only inspection. Generate the request from the immediate next action; name the actual app or site, the exact UI action, and the task outcome. Ask and wait: "I notice computer use is available. Would you like me to use it to [action] in [app/site] so I can [outcome]?" Never reuse a generic dashboard example when it does not match the task.
- For API-key setup, say which provider's API-key page you want to open and whether you intend to navigate, create a scoped key, or verify configuration; explain that the user retains control when the secret is shown or copied. For an authorization window opened by `terse integrate connect`, say which integration is being connected and ask to review and continue that specific consent flow, handing control back for sign-in or MFA.
- Treat an explicit request to use computer use for the current task as permission for that stated scope; do not ask redundantly. Otherwise, availability alone is never consent.
- Once approved, proceed with routine in-scope navigation, inspection, screenshots, verification, clicking, typing, and uploading without asking for every step. Ask again before expanding to another app, account, surface, or materially different goal.
- If the user declines or does not answer, do not use computer use. Continue with native tools when possible, or explain the smallest manual step needed.
- Ask immediately before any consequential external action the user has not already explicitly authorized: sending or publishing, submitting a consequential form, deleting, purchasing, changing permissions or security settings, or modifying production data. A clear request to perform that specific action is approval for its ordinary in-scope UI steps; do not ask again for every click.
- Begin an OAuth connection only when the user explicitly asks to connect that integration. Navigate the consent flow when authorized, but hand control to the user for passwords, MFA codes, payment details, or other sensitive fields; never read, reveal, or enter them through computer use.
- Stay within the requested surface and avoid opening unrelated private content. After an action, inspect the resulting screen and report what actually happened.
- Keep source-code edits, exact structured reads, bulk or repeatable operations, and Terse CLI commands in their native tools. Use a dedicated connector or API when the UI cannot perform the task reliably or safely.
- Fall back gracefully when computer use is unavailable, the app is not installed or signed in, the surface cannot be controlled, or the UI would expose secrets or increase risk. Explain the smallest manual step the user must take, then continue automatically.
- Preserve every confirmation and testing-safety rule below. Computer use changes the interaction surface, not the permission boundary.

## Steps

**Narrate the run.** Speak to the user in casual first person about the work itself: "I'm going to check if the project is set up", "I need some help understanding this use case better", "I'm launching some research agents to figure out how to build this for you", "Based on the research, I have a few more questions". The steps below group into phases — bootstrap (step 0), interview (1), research (2), shared understanding (3), design (4–8), build (9), swap and verify (10–11), deploy (12). Step numbers are internal and never spoken, but the phases are not: at every phase transition, say in plain casual words which phase just finished and which comes next, with its place in the overall sequence — "Research is done — that's 1 of 5 for this build. Next up: agreeing on the design." The user should always know where the build stands from the prose alone.

If your harness has a task-tracking tool the user can see (a todo/task list), keep a checklist too: create it right after step 0 with casually worded items — research how to build this, agree on the design, build the job, swap test targets and verify, ask about deploying — and check items off as phases complete. The checklist supplements the spoken phase transitions; it never replaces them.

**Mark every CLI run.** The `terse` CLI doing something is an event the user must be able to see; harnesses often collapse tool calls, so the narration carries it. Immediately before each `terse` command, emit a marker line:

> ⏵ `terse test run pr-triage --id 3f2c` — running the job on a sample PR event

One marker per command: the command verbatim in backticks, then the why in a few words. Back-to-back cheap reads (`terse test list`, `terse test show`) may share a single marker. When a state-changing or long-running command finishes (`integrate connect`/`wait`, `generate`, `deploy`, `test run`), report its outcome in the normal narration voice. Never bury a `terse` invocation inside a pipeline, subshell, or script where the user can't see it ran.

**Explain every test before it runs.** A marker line is not enough for `terse test run` or `terse replay`: immediately before one, say in one to three casual sentences what the run exercises, why now, and what a good result looks like — "I'll run a sample PR event through the job now and expect a triage summary posted to the test channel with no errors." When it finishes, give the verdict against that stated expectation in the same voice. Cheap reads (`terse test list`, `terse test show`) keep just the marker.

If `terse test run` prints a `TERSE TEST REPORT`, surface it immediately in your own message. Your first sentence must be either "The test ran as expected" or "The test did not run as expected." Then list the reported side effects exactly and explain any mismatch with your stated expectation.

Narration is non-blocking: never wait for a reply to an announcement; the run only pauses at the questions the steps themselves define.

**Headless runs.** With no one to answer, skip every question the steps define, take your recommended answer, and state each choice with its reason in the final summary. Narrate inline the same way.

### 0. Ensure a Terse project exists

Look for `terse.config.json` or `src/terse.generated.ts` in the working directory. If either exists, the project is set up — skip to step 1.

If the user asked to self-host Terse rather than build a workflow on Terse Cloud, that is a different flow — hand off to the `terse-self-host` skill instead.

If neither file exists, this is a fresh start: bootstrap a new SDK project pointed at Terse Cloud with `terse init`. Terse Cloud hosts the control plane and the data plane.

**Run `terse init`.** Don't preflight. Don't run `find`, `ls`, `terse --version`, or `terse integrate list` to "check the environment" before doing anything. Just run `terse init` and react to whatever it tells you. The CLI already knows how to detect existing projects, missing auth, taken directory names, and a missing `package.json`. Trust its error messages and recover from them.

```bash
terse init <project-name>   # scaffold into a new subdirectory
terse init                  # scaffold into the current directory (must be empty of npm files)
```

If the user named a project directory, pass it; otherwise scaffold into the current directory.

Set the Bash timeout to 600000 (10 minutes). Dependency install and browser-side auth can both take a while.

`terse init` handles auth itself: if the user is not logged in, it opens the browser to WorkOS and waits for them to authorize. Before running it, tell the user casually that a browser tab may pop up and that the setup pauses until they finish signing in there. If the CLI prints a login URL and the user seems stuck, relay that URL as a clickable fallback — the automatic browser open fails silently over SSH or in a container. The single command scaffolds files, installs dependencies, logs in if needed, creates the remote Terse project, writes `terse.config.json`, and runs `terse generate`.

**React to errors.** If `terse init` fails, read the error and recover:

- **`command not found: terse`**: install the CLI with `npm i -g terse-cli` (or `pnpm add -g terse-cli` / `yarn global add terse-cli` if the user prefers), then retry `terse init`.
- **`Detected an existing npm project in this directory`**: the directory has a `package.json`. If it also has a `terse.config.json` or `src/terse.generated.ts`, the user is already set up: stop, do not re-init, and continue with step 1. If it is some other npm project, recommend `terse attach` (adds Terse to an existing project) or rerun with a project name to scaffold a sibling directory.
- **`Directory "X" already exists`**: pick a different project name, or have the user `cd` into the existing directory so you can evaluate from there.
- **`ACTION REQUIRED: Not authenticated`**: only happens in non-interactive / CI mode when no valid stored credentials are present. Run `terse auth login` via Bash with a 10-minute timeout, then retry `terse init`.
- **`Failed to create Terse project`**: the API key is valid but the remote create failed. The local scaffold still exists. Surface the error to the user and suggest retrying after fixing connectivity, or creating the project from the dashboard and writing `terse.config.json` manually.

**Continue.** The CLI prints a "Next steps" block at the end of a successful `terse init`. Don't repeat it verbatim. Connect any integrations the requested workflow needs — run `terse integrate describe <type> --json` first to learn whether the install is form or OAuth and which fields it requires, then `terse integrate connect <type>` per step 7. Then continue with step 1 and build the workflow the user asked for.

`src/terse.generated.ts` and the `src/terse.generated/` folder are the source of truth for connected integrations, available triggers, skills, resources, and deterministic wrappers. The comment at the top of the root file lists every integration currently available in Terse — never plan around an integration that is not in that list — and indexes the per-integration files with the exact file and line for every tool, trigger, and resource. Do not run `terse integrate list` — the generated files already reflect what `terse integrate` connected or what is available to connect in this workspace. For what an available integration can do, `terse integrate tool <type> --json` lists its tools with descriptions, and `terse integrate tool <type> <tool-name> --json` returns one tool's input/output schemas. To see what a tool actually returns, `terse integrate tool run <tool> --params '<json>'` runs it one-shot — see "Testing tool calls from the CLI" in the testing conventions.

If `src/terse.generated.ts` is missing in an existing project, run `terse generate` before inventing helpers. If it exists but does not expose the helper you need, or the header index disagrees with what a leaf file actually contains, rerun `terse generate`. Never edit the generated files directly.

### 1. Initial interview

Interview the user before doing any research: their answers set the direction the workspace scan and the researchers take in step 2.

Ask about decisions and intent only — what should happen, on which events, for whom, and what a good outcome looks like. Do not ask about how to implement it: what is connected, which triggers exist, what the docs support, and what an external API offers are exactly what step 2's scan and researchers retrieve. If the request already pins down the intent unambiguously, skip straight to step 2.

Batch related questions, at most four per interruption. For every question, provide a recommended answer.

While interviewing, sharpen the domain language:

- When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."
- Stress-test domain relationships with concrete scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.

### 2. Scan the workspace, then dispatch researchers

**Workspace scan — always yours, never a subagent's.** The exact types and signatures must be in your context to write the job in step 9, so delegating this read would only add a wasted pass. Read, in full:

1. `src/terse.generated.ts` (the root file). The header lists every integration currently available in Terse and carries the line-numbered index of the `src/terse.generated/` folder; the re-exports below it reflect what is connected.
2. `src/terse.jobs.ts` (or the project's custom entry file, or the legacy `src/index.ts`) — note existing jobs that overlap or conflict with this workflow.

Then follow the index into the leaf files selectively: for every integration this workflow will touch, read its `terse.generated/<integration>.triggers.ts` and `<integration>.tools.ts` in full before any code is written against them — step 9 needs the exact signatures in your context. Skip only the files for integrations the workflow does not involve. If the index disagrees with what a leaf file actually contains, treat the whole bundle as stale and rerun `terse generate`.

From the scan, carry forward the exact trigger factories (`Triggers.*`), skill factories (`Skills.*`), `toolbox.<integration>.<method>` signatures, and resource constants that match this workflow — only names that literally appear in the generated files, never invented — plus the gaps. Classify each gap against the available-integrations comment: a platform in the list but with no generated files under `terse.generated/` is a connectable built-in (`terse integrate tool <type> --json` lists its tools); a platform absent from the list has no Terse built-in — leave those to the researchers below.

**Researchers** are focused, read-only background research tasks defined by self-contained templates in `references/`. Each produces a research brief in a fixed shape.

- **Integration researcher** ([research-integrations.md](references/research-integrations.md)) — dispatch only after the scan confirms a needed service has no built-in Terse integration (rungs 3–4 of the integration ladder in the conventions section). Researches the platform's official SDK, auth model, and key endpoints.

**Dispatching a researcher.** For each one:

1. Read its template file from `references/`.
2. Replace the "Context from the orchestrator" comment block with real content: the workflow in one paragraph, the platforms/events/actions involved, and the interview answers and scan findings that narrow the search. Where the template has an "Objectives" block, fill it with the specific questions this run needs answered — researchers answer the objectives with the fewest fetches that settle them, so vague objectives buy back the over-reading.
3. If your harness can spawn subagents (an Agent/Task tool), pass the entire filled-in template as the subagent's prompt, using a read-only, web-capable agent type. The template is self-contained: the subagent needs no other context, and its reply is the research brief.
4. If it cannot, follow the filled-in template yourself, inline, and write out the same brief before moving on.

**Background where the harness allows.** Dispatch the integration researcher as soon as the scan justifies it — it can run while you conduct step 3. Inline execution runs the template before moving on.

Researchers are read-only: they must not edit files or run state-changing commands (`terse integrate connect`, `terse generate`, `terse deploy`).

### 3. Reach a shared understanding

Combine your workspace scan with any research briefs, then bring the user everything that changes the design:

- Decisions the research raised: a missing integration to connect or work around, two plausible trigger choices, a capability the research surfaced that suggests a different shape for the workflow.
- Contradictions between what the user asked for and what exists. Surface them; do not silently resolve them.

The same interview rules apply: batch at most four questions, recommend an answer for each, look up facts instead of asking. Walk down each branch of the design tree, resolving dependencies between decisions one by one.

Once the decisions are resolved, draw the workflow as an ASCII diagram: one box per step with its title and the one or two details that matter (marked `agent:` where judgment is involved), the trigger at the top, the side effects at the end. Decisions live on the edges, not in boxes: a filter is a label on the connector, a stop is a `├─►` exit off the spine, and a real branch is two side-by-side boxes that merge back into the spine. Use the domain words from the interview, not code identifiers, and keep it narrow enough for a terminal:

```
┌──────────────────────────────────────────────┐
│ Attio deal record updated                    │
│ object: Deals                                │
└──────────────────────┬───────────────────────┘
                       │ filter: stage = "Negotiation"
                       ▼
┌──────────────────────────────────────────────┐
│ Does #war-<deal-name> already exist?         │
└──────────────────────┬───────────────────────┘
        already exists ├─► stop, the war room is already up
                       ▼ new deal
┌──────────────────────────────────────────────┐
│ Create private channel #war-<deal-name>      │
└──────────────────────┬───────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────┐
│ Resolve the invite list                      │
│ fixed group: Olivier + Thomas                │
│ deal owner matched to Slack by email?        │
└──────────┬───────────────────────────┬───────┘
           │ matched                   │ unmatched
           ▼                           ▼
┌─────────────────────────┐  ┌─────────────────────────┐
│ Add the owner to the    │  │ Invite the fixed group  │
│ invite list             │  │ and post a heads-up     │
└────────────┬────────────┘  └────────────┬────────────┘
             │                            │
             └──────────────┬─────────────┘
                            ▼
┌──────────────────────────────────────────────┐
│ Invite the deal team to the channel          │
└──────────────────────┬───────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────┐
│ agent: write the deal brief                  │
│ pull value, stage, contacts from Attio       │
│ post and pin the brief in the channel        │
└──────────────────────┬───────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────┐
│ Schedule the kickoff call                    │
│ location: Google Meet                        │
│ invites emailed to the deal team             │
└──────────────────────┬───────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────┐
│ Post the calendar link in the channel        │
└──────────────────────────────────────────────┘
```

The diagram is a separate step from the design questions: resolve every remaining decision through the question batches first, and only then draw it. Send it as its own message with the confirmation ask in plain prose ("Does this match what you have in mind?") and end the turn there, with no tool call in that turn — a question dialog takes over the screen and buries same-turn text, and its preview pane clips tall content on small terminals, so the question tool is never used for this confirmation. The diagram is the shared understanding made visible, and the spec the build is written against. Do not start building until the user replies confirming a diagram they have actually seen.

### 4. Open the entry file

Open `src/terse.jobs.ts` (the canonical job entry file). The CLI can still load `src/index.ts` as a legacy fallback, and custom layouts can override the entry file with `--entry-file`.

If the repo already uses a custom entry file, follow that layout and pass `--entry-file` on later `terse` commands.
Job placement follows the project-layout rule in the conventions section: every job lives in its own `src/jobs/<kebab-case-name>.ts` file, and `src/terse.jobs.ts` is a manifest of side-effect imports — create the new job as its own file in `src/jobs/` and add its import line. If the repo still keeps a job inline in `src/terse.jobs.ts`, move it to its own file in `src/jobs/` when you touch it.
If the repo only has `src/index.ts`, treat that as a legacy fallback instead of creating a second competing entry file.
If no runtime entry exists yet, create one:

```typescript
import { createJob, generateText } from "terse-sdk"
```

### 5. Settle durability

Durability changes how the handler is structured, so settle it before writing any handler code. Check the confirmed design against the two signal kinds in the conventions section ("When to be durable").

**A forcing signal is present** (the design waits on human input or time): durability is not a decision, so never ask. State it as a consequence in one line and move on: "This job waits for \<approval/the timer\>, so it runs durable."

**Only the judgment signal is present** (three or more side-effecting stages, no waits): this is a real trade-off, so ask, presenting your recommendation as the default. Use this copy:

> **Should this job be durable?**
>
> - **Durable** — survives restarts, can pause and wait. e.g. "When a refund over $500 comes in, wait for manager approval in Slack, then process it." The job can sleep for days without losing state.
> - **Non-durable** (default) — a closure in the cloud. e.g. "When a PR opens, post a summary to Slack." Runs once, fast, no constraints; a crash just means that one run failed.
>
> Tradeoff: durable requires every side effect to live in a `step()` and all step data to be serializable.
>
> Recommended for this workflow: \<your recommendation and one-line reason\>

**Neither signal**: the job is non-durable. Don't ask and don't announce it.

For durable jobs, the style rules in the conventions section ("Durable job style") govern the handler; the mechanics (`step()`, `jobStep`, `sleep`, `waitForInput`, replay model) are in https://docs.useterse.ai/core-concepts/durability.

### 6. Pick triggers

Choose triggers based on what events the job should respond to. Your workspace scan surfaced the candidates; import trigger factories and resource constants from `./terse.generated`.

Only use triggers and resources that actually exist in the generated files. Do not invent constants that are not defined there.

When the user gives a schedule in their local time, keep it in their timezone: `Triggers.schedule.cron` takes an optional `timezone` (a typed IANA name like `"America/New_York"`, autocompleted from the `Timezone` union) and evaluates the expression as wall-clock time in that zone, DST-aware. Do not convert the user's time to UTC by hand; omitting `timezone` means UTC.

### 7. Pick skills and connect integrations

**Skills shape what the model can do during a `generateText` run.** They scope the tools the model is allowed to pick. `toolbox.*` is unscoped and works without any skills at all.

Rules of thumb:
- If a step is fully deterministic, call `toolbox.<integration>.<method>` directly. No skill required.
- If the model needs to choose actions on an integration during a `generateText` run, that integration must be in `skills`.

When the workflow needs a platform, step 2 has already walked the integration ladder in the conventions section ("Integrating with a platform"): the workspace scan settles what is connected and whether a built-in type exists, and the integration brief covers the external rungs. Confirm the rung and stop at the first one that works; for the community-wrapper rung, put the choice to the user with the brief's evidence.

Connecting a missing built-in integration type:

- **Always describe before connecting.** Run `terse integrate describe <type> --json` first: it returns the `installationType` (form or OAuth), and for form installs the exact fields the connect command requires — each field's name, type, whether it is required, and a hint — plus any setup URL for console work the user must do first. Never guess field names.
- For form installs, collect a value for every required field from the describe output — ask the user for the ones you cannot derive, relaying the field hints and any setup URL — then run `terse integrate connect <type> --field key=value --fields-stdin`
- Put secrets on `--fields-stdin`, not `--field`
- For OAuth installs, you are the user's only messenger: in `--json` mode the CLI's own "ACTION REQUIRED" line is suppressed, and the browser open is best-effort (it fails silently over SSH or in a container). So, in order:
  1. Tell the user first, casually, what is about to happen and that the run pauses until they act: "I'm connecting Gmail now — a browser tab will pop up; finish the sign-in there and I'll pick it back up."
  2. Run `terse integrate connect <type> --json`. It opens the browser and exits 2 with a `handoff` payload containing the authorization `url` and a `waitCommand`.
  3. Give the user the `url` from the payload as a clickable fallback: "If no tab appeared, here's the link: <url>".
  4. Run the `waitCommand` (e.g. `terse integrate wait gmail`) to block until they finish authorization — it exits 0 when the connection is live. Only then continue.
- If you need multiple OAuth integrations, do them one at a time: announce → `connect <a> --json` → `wait <a>` → announce → `connect <b> --json` → `wait <b>`. Do not batch the connect calls; the user can only authorize one browser tab at a time anyway.
- After any connection or refresh, rerun `terse generate` and reread the header index plus the leaf files for the integrations this workflow touches

For anything past the built-in rungs, credentials follow the Credentials rule in the conventions section ("Integrating with a platform").

When those credentials take manual console work (creating a service account, generating a key, sharing a resource), deliver the integration brief's setup walkthrough at the moment the user actually needs to act — not earlier, not compressed. Relay its numbered steps with their links and role/scope grants intact; do not paraphrase them into vaguer steps. Append the exact `terse secrets add` command for each secret, following the Credentials rule (the base64 one-liner for file credentials). Steps the brief marked `(unverified)` keep that marker when relayed, so the user knows which links or role names to double-check instead of trusting them blindly.

### 8. Consider a filter

Add a `filter` function if the job should skip certain events:
- Bot-generated events (`event.sender.login.includes("[bot]")`)
- Draft PRs, WIP items, or specific labels
- Events from specific users or repos

Filters prevent unnecessary agent runs and save cost.

### 9. Build the handler

The "Terse Job Code Conventions" section at the bottom of this file governs every line of handler code below. Exact names and signatures come from your step 2 read of the generated files, never from memory of the design discussion; reopen them only if `terse generate` has rerun since the scan (step 7 reruns it after connecting an integration).

The worked examples in the conventions section (durable and non-durable) show the target shape.

Which primitive a piece of work belongs in:

- **Deterministic work** — map each known action to `toolbox` (no agent):

  ```typescript
  import { toolbox, SlackChannel } from "./terse.generated"

  await toolbox.slack.sendMessage({
      channelId: SlackChannel.Engineering.channelId,
      message: `New PR: ${event.pullRequest.title}`,
      thread_ts: "",
      blocks: "",
  })
  ```

- **Agentic work** — use `generateText` wherever judgment is required (summarize, triage, draft). Include full event context via `event.formatForAgentRunner()`; write specific prompts, and pass an `outputSchema` (a Zod schema) to get a typed, validated object back. Reach for `TerseAgent.create()` only for streaming with `run()` or reusing one agent instance across calls.

- If the user's request is fully deterministic (e.g. "post X to Slack when Y happens"), do not create an agent at all.

**Test targets.** Before the first test run, if the job writes to an external surface, ask the user once: "Which channel/repo should I use for test runs? (I'll swap to the real targets at the end.)" In the same ask, when an involved API bills or has customer-visible effects, ask whether it has a test-mode key to use while building (`terse secrets add <NAME>`). Point side-effecting calls at those test resources while building. If the user says to use the real ones, proceed live. The "Testing Safety Conventions" section at the bottom of this file governs every test run — real data, test keys, production writes, probing.

**Running the job locally.** `terse test` replays a stored trigger event through the real handler:

```bash
terse test list "<job-name>" --json
terse test show <id> "<job-name>" --json
terse test run "<job-name>" --id <id>
```

`terse test list` lists the stored sample events and `terse test show` prints one. `terse test run --id <id>` runs that event; `--event-file <path>` or `--event <json>` runs a payload you supply. If multiple jobs exist, pass the job name explicitly — non-interactive job loading cannot prompt. Reserve bare `terse test` for manual sessions with a TTY.

When the sample buffer is empty (a fresh webhook has no stored deliveries), hand-write the event file: on a shape mismatch, `terse test run` prints the expected envelope and the exact validation issues — correct the file from that error output rather than guessing fields.

Exit code 0 only means the handler ran. Where an agent produced text, read the output and judge it against the prompt's intent.

If the build deviated from the confirmed design, say what changed and why. Do not wait for a reply; if a checklist is active, check the item off.

### 10. Swap to real targets

Enumerate every test resource and test-mode secret you pointed at in step 9 and switch each back to the real one — list them explicitly so none is missed.

Ask the user if they want one last run against real production data or just deploy the workflow. 

This run fires the real side effects once; tell the user before running it.

### 11. Final check

Verify the things that are easy to get wrong:
- `pnpm exec tsc --noEmit` passes (use `npx tsc --noEmit` if that matches the project).
- Every `Triggers.<integration>.…`, `Skills.<integration>(…)`, resource constant, and tool call exists in the generated files. Inventing constants that aren't there is the most common failure.
- Known calls use `toolbox`; only steps that need judgment use `generateText`. `TerseAgent.create()` appears only when streaming or reusing an agent.
- `skills` lists every integration the model must call during a `generateText` run.
- Agent prompts include full event context via `event.formatForAgentRunner()`.
- Imports resolve to real exports from `terse-sdk` and `./terse.generated`.
- The code follows the conventions section: no stray casts, exhaustive switches, custom error classes, stepdown ordering, types at the bottom.
- Durable jobs: every side effect lives in a step, branch helpers sit below the job in the same file, only serializable data crosses step boundaries.
- Every test target from step 9 is swapped back to the real resource.

### 12. Ask before deploying

Do not run `terse deploy` automatically. After the job is written and verified, ask the user whether to deploy it now.

Example prompt:

> The workflow is ready locally. Deploy it to production with `terse deploy`? (This syncs all jobs in the project — removed jobs are deleted remotely.)

- If the user says yes, run `terse deploy` and report the outcome.
- If the user says no or wants more changes, stop without deploying and remind them they can run `terse deploy` when ready.

---

# Testing Safety Conventions

These conventions govern every local execution of a job — `terse test run`, `terse replay`, and ad-hoc tool calls (`terse integrate tool run`). Local runs execute the real handler with real credentials: nothing about a test run is sandboxed unless you point it somewhere safe.

## Real events in, test targets out

Sample events (`terse test list`, `terse replay`) are real production data; using them as *inputs* is the point — realistic payloads catch real bugs. The line is side effects: a test run must never land writes on real people or real surfaces. Emails, messages, ticket updates, CRM writes — during test runs these go to the test targets the user named, never to the customer, channel, or record in the event.

The one sanctioned exception is the single verification run after swapping to real targets, announced to the user before it fires.

## Test API keys

When the work touches an external API that bills or has customer-visible effects (Stripe, Resend, …), ask the user once — fold it into the test-targets question — whether the platform has a test or sandbox key: "Does this have a test-mode key I should use while building? If so, add it with `terse secrets add <NAME>`." Use the test key for every local run; swapping to the live secret is part of the final swap to real targets.

Secret values are write-only: never try to read a stored secret to check whether it is a test or live key. Discovery is by asking, not inspecting.

## No test key: reads are free, writes ask first

Without a test key, local runs share credentials with production:

- **Reads never need permission.** Listing, fetching, and querying production data during a test run is always fine.
- **Writes ask per surface.** Before the first local run that writes to a production surface (a real repo, a live audience, a customer record), ask the user explicitly about that surface. One ask covers later runs against the same surface; a new surface is a new ask.

## Testing tool calls from the CLI

To learn what an external API returns — the shape of a record, the options a status field takes, which channels exist — run the tool from the CLI:

```
terse integrate tool run slack.listChannels
terse integrate tool run attio.records --params '{"request":{"action":"query","objectSlug":"deals","filter":null,"limit":5,"offset":null}}'
```

- Name the tool by wire name (`attio_records`) or dotted form (`attio.records`); a wrong name errors with the list of valid ones.
- `--params` takes the tool's wire-shape JSON — the exact input schema `terse integrate tool <type> <tool-name> --json` prints. Read that schema, not the generated `toolbox.*` signatures, which can differ from the wire shape. Pipe large params on stdin instead of `--params`.
- The connection is auto-resolved from the project's pinned connection (`terse integrate use <type> <connection-id>`) or the workspace's single connection; with several and no pin, list IDs with `terse integrate connections <type> --json`, then retry with `--integration <id>` or pin one.
- The result prints as raw JSON on stdout; failures exit nonzero with the error on stderr.

Keep discovery runs read-only: query, list, get. Writes belong in the job, governed by the sections above.

For an API with no toolbox tools, where the call needs hydrated secrets, run the reads inside the existing job:

1. Insert one contiguous block at the top of the job's handler that does the reads, logs the results, and ends with `return` — nothing below it executes.
2. Run it with `terse test run`, so secrets hydrate exactly as they do for real jobs.
3. Record what it found, then delete the block and confirm with `git diff` that the handler is byte-for-byte back to its previous state.

Never create a separate throwaway job for this, and never deploy while the block is in place — `terse deploy` syncs every job in the project.

What discovery finds lands in job code as named, explicitly typed constants — see "Discovered values are typed constants" in the code conventions.

---

# Terse Job Code Conventions

These conventions govern every line of job code you write or modify in a project's job files (`src/terse.jobs.ts` and `src/jobs/`). Precedence: live Terse docs win on facts (API signatures, CLI flags, availability); this file wins on style.

Overrides the user gives in the session win over this file.

## Type discipline

**Type everything; rarely cast.** Reach for `as` or `any` almost never. A cast usually means the approach is wrong: refactor at a higher level, fix how the third-party dependency is wrapped, or ask the user what to do. Prefer type guards, generics, and `satisfies`.

**Zod at trust boundaries.** A zod schema (with `z.infer` for the static type) is required exactly where data crosses a boundary you don't control:

- responses from external APIs not typed by an official SDK
- webhook trigger payloads
- `jobStep` `inputSchema` / `outputSchema`
- `generateText` `outputSchema`

Internal shapes you construct yourself stay plain types. Official SDK types are trusted as-is; don't re-validate them at runtime.

Derive the static type from the schema so there is one source of truth:

```typescript
const Classification = z.object({
    severity: z.enum(["critical", "routine"]),
    reason: z.string(),
})
type Classification = z.infer<typeof Classification>
```

**Explicit return types.** Every function declaration and named arrow function gets an explicit return type, helpers included. Inline callbacks — `map`/`filter` lambdas, the `filter` and `onTrigger` functions passed inline to `createJob` — stay inferred.

**Discovered values are typed constants.** Values discovered by probing external state (an audience ID, a verified domain, a channel ID) land as named constants with explicit types, never as bare string literals inline in a call. Use the SDK's type when it has one, otherwise a narrow alias:

```typescript
const WAITLIST_AUDIENCE_ID: AudienceId = "78261eea-

…(truncated)
