--- name: gtm-workflow description: Triggers when a user asks to build, create, update, run, test, schedule, deploy, host, upgrade, inspect, open the inspection UI, or delete a saved GTM workflow, its result table, runs, or diagram in a GTM workspace, with phrasings like "build a workflow that scores our inbound companies", "run Score inbound accounts", "put it on a weekly schedule, hosted", "enrich my network, connections, or followers and their companies", "upgrade the workflow runtime", "take our workflows live on Vercel", "connect the workflow project", "set up keys", or "share this workflow's data". Owns the workflows folder, which holds workflow code, tables, runs, schedules, diagrams, keys and share links, and the Vercel project it deploys to, including connecting a GTM agent to it. Not for the workspace, ICPs, or personas themselves (gtm-workspace, gtm-icp, gtm-persona), or one-off fit checks that are not saved (gtm-qualify-prospects). metadata: version: "0.2.6" --- # GTM Workflow ## Trigger Apply this skill when a request concerns a saved workflow: creating, changing, running, scheduling, deploying, upgrading, inspecting, opening its UI, or deleting one, or its table, runs, or diagram. When creating or changing a workflow, check [recipes](recipes/README.md) (e.g. network enrichment) for one that matches the request and follow it; this skill still owns the lifecycle. Ordinary runs of an unchanged workflow need no recipe. Show the intent menu only when the user's entire request is `/gtm-workflow` or `/gtm-workflows`, with no action or other words. Ask **Choose a workflow action.** with three choices: **Open GTM Workflows**, **Create a workflow**, **Manage a workflow**, and wait. Manage asks which workflow and action: change, run/test, schedule/deploy, upgrade/repair, or delete. Bare entry performs no creation, execution or deployment. **Open GTM Workflows** is already an explicit Open request. Resolve the verified viewer URL and return it immediately using [entry links](references/entry-links.md); do not show the intent menu or ask the user to choose Open again. Loading this skill does not turn an explicit request into a bare invocation. Other explicit actions also go directly to their procedure. If Open finds no workspace, explain workspace setup; if the workspace has no workflows, show its empty list. ## Scope This skill owns `/workflows/`, a Vercel Workflow runtime on Nitro copied from `templates/` on first use: workflow files, `db/tables/`, migrations, `vercel.json`, `.env`, the local database, runs, diagram pages, and the deployed copy. Runs execute locally against this workspace's own Postgres in `data/pg` by default; they target the deployed copy when the user asks or the host states there are no local runs, and hosted results are read through the deployed copy's query route. Keys live in `workflows/.env.local` locally and in the project's Production variables when deployed, both edited through the viewer's Keys page (Connections tab); for setup, key discovery and key changes, read [Keys](references/connections.md). The remaining workspace files outside `workflows/` belong to the other gtm skills. ## Inputs The request; the workspace found by [the contract](../gtm-workspace/references/contract.md); the ICPs and personas the workflow reads by slug; `references/local.md` for commands, keys, and verified facts; `references/deploy.md` for hosting. ## Roles The user's request authorizes routine code changes and real runs within the configured row and cost caps. State the estimate and proceed; confirmation is reserved for destructive actions under the shared interaction rules. The workflow project goes live through shared setup `--deploy` ([deploy.md](references/deploy.md)), run directly or as the last step of optional `gtm-agent` setup; the user chooses the AI backend once per workspace in `.env`; the agent writes, runs, and reports. Deploy is the push. ## Procedure When persisting enriched people or companies, follow [shared profiles](references/shared-profiles.md) for storage, identity, freshness, paid attempts and scoped Data views. Its two phase workers and common budget replace the generic self-fan-out pattern below for multi-phase enrichment. The profile tables are runtime tables in schema `gtm`; `scripts/migrate.mjs` migrates them with everything else, and upgrades preserve them. The first gtm skill used in a conversation checks once for a newer release, as [updates](../gtm-workspace/references/updates.md) says. Talk by the six rules in [interaction](../gtm-workspace/references/interaction.md); reproduce [the dialogues](references/interactions.md). Workflow slugs follow the contract's slug rule; the display name is the doc comment's first line. | Job | Do | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Create | First use on a personal computer: run shared local setup from [Keys](references/connections.md), which creates an empty registry and prepares the local database without execution. Hosted authoring uses the already configured runtime. Start `npm run dev` (the one local command: viewer, Keys page and runs; opening it runs nothing). Ask where the rows come from. When the workflow reaches people (an approval on a stage, or a `notify` call), ask which Slack channel it should post in, one question: a channel id (`C0…`, shown at the bottom of the channel's About tab) or "here", which means the channel of this conversation when the agent knows its id; the answer becomes a `NOTIFY` constant at the top of the workflow file, passed as `notify` to every stage and `notify` call, never a project variable. There is no per-workflow backend question: agent stages read `GTM_AGENT_BACKEND` from `.env` at run time (`claude` or `codex` for the author's subscription on a personal computer, unset for AI Gateway), so one file runs on the subscription locally and on the Gateway hosted. At scaffold on a personal computer, when `claude` or `codex` is on PATH and no Gateway key is present, set it once and say so; otherwise leave it unset. Author literal `viewer.businessGraph` and `viewer.description` (about 280 characters, at most 500) using [viewer](references/viewer.md), with labels and branches checked against the new code. Write `workflows/.ts` from `example-scores.ts`, or from `example-research.ts` when it has an agent stage, its table in `db/tables/.ts`, register both in `workflows/index.ts` and `db/tables/index.ts` (keep the example workflows and their tables; removing a table in the same generate as adding one makes the generator ask a rename question that no host can answer), run `npm run db:generate -- --name `, run `npm run viewer:register` once to assign the new workflow identity, rebuild, open its Diagram page, commit, close with what was created and the Open GTM Workflows entry. On a host without local runs: no localhost link, ever; after the push, read `GET /api/link/` on the deployed copy every 10 seconds, up to 3 minutes, until `git merge-base --is-ancestor HEAD ` holds for the `commit` it returns, and only then post its verified viewerUrl; a reply written before that holds carries no links and says the deploy is under way. When the user asks to run, execute the requested scope after checking keys and caps, stating the estimate without another confirmation; when no connected project exists on a host without local runs, close instead by naming the connection steps in [deploy.md](references/deploy.md). | | Update | Change the workflow, its `viewer.businessGraph` and its `viewer.description` together, including any new business decision branches and owner details; preserve `viewer.id`, validate the metadata with `node scripts/build-viewer.mjs`, move an `export const criteria` copy to `readIcp`/`readPersona` (see Criteria below), add columns (then `db:generate`; a running `npm run dev` applies it); preserve existing data; close with what changed and the Open GTM Workflows entry. | | Run | Target: local by default on a personal computer; the deployed copy when the user asks or the host states there are no local runs, which needs a connected project and, first, Deploy's readiness check. Pre-run check: the row count, rows × estimate against both caps, every connection the workflow uses against the target inventory in [Connections](references/connections.md), and whether the workflow is new or changed since its last run. A requested run, whether one or many rows, proceeds without a permission or test-first question: state the row count and estimated cost, then run the requested scope. A missing key or exceeded cap stops execution and names the blocker. Start through the route, poll the read route, close with the result in plain words: done, failed, skipped, cost; no links, unless they changed since the thread last showed them. When the read route lists a pending approval, stop polling and ask the person in one message (the tool, its input in plain words, the price when known), then decide it through the approve route with their answer and its reason, and resume polling; a run with `stream` on may be narrated from the stream route while it runs. When a local run follows a hosted one, say it may re-spend on rows the hosted copy already did. | | Open UI | For “Open GTM Workflows UI” or an explicit link request, follow [entry links](references/entry-links.md). Resolve the discussed workflow by stable identity; without one, open the list. Check readiness. Locally start `npm run dev` if needed; its Connections tab (`/connections`, the address to give for keys) is the Keys page on the same server. Opening the UI never executes a workflow, runs a migration, or deploys a missing host. Report a concrete setup blocker when unavailable. Always return the entry when explicitly asked, even if already shown. | | Share | Hosted only: read `grants` using `/api/viewer/service?v=3&workflow=&op=grants`. Show the selected tabs and the current permitted-data summary before an explicit save. POST `saveLink` with `{views, policy, save}`. `policy` is the displayed hash; `save: true` explicitly approves scope or policy changes. Copy without changes reuses the link. Default is Diagram with no expiry. POST `revokeGrant` with the active ID to turn it off. Re-enable creates a fresh secret. Use the exact returned URL. See [viewer](references/viewer.md). | | Deploy | Deploy is the push; follow [deploy.md](references/deploy.md) for the one-time connection and the readiness check. A push never touches a run already in flight: it finishes on the code it started with. The hosted copy runs every agent stage through the Gateway whatever `.env` says locally. | | Upgrade | First run `node scripts/upgrade-package.mjs ` from this skill's folder (a preview, it writes nothing): when this skill's template is older than the workspace it refuses, and the upgrade stops there with "the skills are behind this workspace; update them first", never a downgrade. Compare `workflows/` to the installed `templates/`; say what differs; a workspace from before the Postgres runtime (it has `db/tables/cache.ts`) is not upgraded by this job: it is migrated as [updates](../gtm-workspace/references/updates.md#migrate) says, after the user agrees; update template-owned `lib/`, `server/`, `viewer/`, `connections-ui/`, `share-server/` (delete a leftover `viewer-server/`), `scripts/`, `drizzle-runtime/`, `nitro.config.ts`, `drizzle.config.ts`, `drizzle-runtime.config.ts`, `tsconfig.json`, `share-firewall.json`; copy `skills/` from the template when the workspace has none; update the workspace root's `.gitignore` and `.github/workflows/check.yml` from `root/`, keeping CI steps the workspace added; follow [runtime reliability](references/reliability.md#upgrade-startup-commands) to merge dependencies and startup scripts into `package.json`, preserving user-added dependencies and custom commands, then copy the template's `package-lock.json` over the workspace's, delete `node_modules`, run `npm install` and confirm with `npm ci --dry-run` (installing over the old lockfile drops other platforms' packages and breaks the hosted build, and a lockfile generated from nothing is rejected by `npm ci`); regenerate `workflows/index.ts`, `db/tables/index.ts`, and `skills/index.ts`, preserving each workflow entry's `viewer`, `data`, `intake`, and other authored properties, including imported metadata files; never touch other files in `workflows/`, `db/tables/`, `skills/`, `drizzle/`, `.env`, `data/`; `vercel.json` is template-owned except its `crons`, which shared setup merges; merge generated-asset and `.env.*` ignore entries (including `lib/criteria.generated.ts`); run shared setup `--local` as [Keys](references/connections.md) specifies; run `npm run viewer:register` only for entries missing an identity; build, start `npm run dev` once so the local database is migrated, then verify the hosted private/share builds. Read each existing workflow and add or correct its businessGraph and description before the build; when one carries `export const criteria` or ICP or persona rules written into its code, move it to `readIcp`/`readPersona` under Criteria, and say which workflows changed. Preserve all workflow IDs. Restart the local server through `npm run dev` and verify its startup settings and a diagram page; hosted upgrades verify the deployed copy after the readiness check and rerun hosted setup, which re-applies the [share rate limits](references/deploy.md#share-rate-limits). | | Delete | Remove the workflow file, its registry entry, and its `vercel.json` cron; keep the table and its data and say so; when a project is connected, say the hosted copy drops it and its schedule as the save deploys. | For Create, Update and Upgrade, read [viewer](references/viewer.md). Keep a valid business diagram and, for every Data tab, a valid `viewer.sharePolicy`. Build before saving; missing or mismatched sharing metadata is an authoring error. Preparing a policy makes Data selectable; changing a public link's access still requires an explicit sharing request. Every save: one sentence on what will change, pull first when `origin/main` exists, edit through the host's write path, commit on `main` with a plain-language message, push when a remote exists, verify the commit (and that it reached `origin/main` when a remote exists), close with what was created, changed, or deleted. On a host that states there are no local runs, the values live in the host's environment (the scaffold never ships `env.example`, on any host); every run targets the deployed copy after the push and after Deploy's readiness check; Upgrade verifies through the deployed diagram page after that check instead of restarting a server. Two rules hold on such a host whatever else the agent does: - Links: a dev server or a local build may be started as a check, but nothing it serves is ever posted. The only links a reply may carry are the verified viewerUrl the deployed copy's link route returned after the readiness wait; `localhost` never appears in a reply. - Keys: read `GET /api/connections` through the protected workflow transport for presence and declared usage, including unused services. Use the saved Note as the service name and the exact variable for code, following [Connections naming](references/connections.md#inventory-and-changes). Direct missing-key entry to the verified `connectionsUrl` from the private link route. Never collect keys in chat. Saved changes become active after a deliberate deployment; presence does not prove validity. ### Conventions the code follows - Linked result tables: [linked data](references/linked-data.md) defines the optional `data` registry property and the read-only People/Companies-style viewer. Register it when users need to browse related records. Its private Data link comes from the same link route. Preserve `viewer`, `data`, and `intake` during upgrades. Sharing is an explicit private-viewer action; it requires a versioned table/column/relationship/row policy described in [viewer](references/viewer.md). - Step choice: every step is the plainest kind that does the job. Deterministic work (parsing, arithmetic, a fetch, a provider or gateway API call with known inputs, a Slack post) is a plain `"use step"` function with no model. Judgment over text the workflow already holds (scoring, classifying, extracting, drafting) is a single-shot AI step, `generateObject` with a schema. An agent stage (`runAgent`) only when the user names an agent, or the work needs the model to choose its own tool calls as it goes (find the right endpoint, research across pages) and no sequence of plain and AI steps can be written in advance; a stage that would always call the same tools in the same order is those steps instead. When the user asks for an agent where steps would do, say so in one sentence and build what they asked. - Rows come from `defaultInput` (inline), a provider or API call inside a `"use step"` function, a table another workflow fills, or an inbound webhook (`export const intake = defineIntake({ secretEnv, signature, eventId, toRow })` from `lib/intake.ts`, registered as `intake` in `workflows/index.ts`; `POST /api/intake/` verifies the sender's signature over the raw body, drops redeliveries by event id for 30 days, and starts one run with the mapped row; senders post to the public share project, which relays signed events past Vercel Authentication; after building or editing one, always tell the user the sender setup in [deploy.md](references/deploy.md#inbound-webhooks)); never from a workspace file or local-only data. Input is `RowsInput` from `lib/rows.ts`, `{ rows?, maxRows?, maxSpendUsd? }`, each row an object with a required string `key`, the table's primary key; the workflow's constants are the defaults; the start route merges a POST body over `defaultInput`, so the limited run is `POST { maxRows: 1 }`. - `runRows({ rows, table, step, maxRows, maxSpendUsd, estimateUsd, freshForMs, concurrency?, saveEvery?, fanOut? })` from `lib/rows.ts` is the loop; `step(row)` is a plain async function in workflow scope that awaits `"use step"` functions in sequence and returns `{ ...columns, costUsd }`. Tables are passed by name and resolved through `db/tables/index.ts`. Finished rows are saved ten at a time in one step (`saveEvery`), because every step costs a hop of the engine; a crash before a save loses nothing, the engine replays finished steps. Every workflow passes `fanOut: { workflow: , input, chunkSize }`: above that many rows the run only splits the list into child runs of itself, four at a time (each wave started in one step), each with its exact share of the caps, and adds up their totals, so a list of any size is one run with one result. `chunkSize` is 100 for a workflow of plain steps and 20 for one with an agent stage: the engine caps a run at 25,000 events and an agent row costs about 100 of them. Cancelling the parent cancels its children. - Every result table has `key`, `updated_at`, `cost_usd`, `error`. All database I/O, every `cached()` and `generateObject` call happen inside `"use step"` functions declared as `async function name() { "use step"; }`, never arrow or method form; every paid or AI step sets `name.maxRetries = 0`. - Reads leave the database once and small: every byte Postgres sends to the app counts as Neon data transfer, so a read names its columns (`db().select({ key: t.key, score: t.score })`, never `select()` on a wide table; `getProfile(db, entity, key, { columns })` for a profile), filtering, counting and aggregation run in SQL (`count(*)::int`, `jsonb_array_elements`, `= ANY(...)`), a step reads what it needs once and keeps it rather than reading a row again per item, a list paginates, and no step pulls a table into Node to loop over it. The budgets every read path is held to are in `lib/read-budgets.ts`; `tests/read-budgets.test.ts` in this skill enforces them, and [local](references/local.md#bytes-out-of-the-database) says how to measure a workflow's own reads before shipping it. - Agent stages: an agent inside a workflow is always `runAgent()` from `lib/agent.ts`, called from a plain async function in workflow scope (the stage function, an `agent` node), never `WorkflowAgent` directly and never inside a `"use step"` function; the helper makes every model call and tool call a step, races the time limit against `sleep()`, cleans the schemas, and returns real cost. Its config is the whole authoring surface: `model` and `reasoning` (leave `model` out: it defaults to `gtmModel()` from `lib/models.ts`, see the next rule; reasoning defaults to `GTM_REASONING`, else `high`), `instructions`, `skills` (names from `skills/index.ts`, each a text module under `skills/`), `tools` (`mcp` servers by name with `url`, `keyEnv`, `allow`, `maxCalls`; `web.fetch` free, a step; `web.search` executed by AI Gateway (`true` or `"gateway"`, Exa, any model, no key) or by OpenAI (`"openai"`, openai/\* models only), inside the model call rather than as a step, so it cannot be approved or call-limited; `custom` tools whose `execute` is a `"use step"` function), `approve` (tool names requiring destructive-action confirmation or an explicitly requested workflow review; never add routine approval gates by default; the row waits on a hook and the run's read route lists the request until `POST /api/runs//approve` decides it; set `timeout` in days when approvals may wait), `stream` (every model and tool event on `GET /api/runs//stream`), `maxSteps`, `maxUsd` (soft: the agent stops using tools after the call that crosses it and writes up what it has), `timeout` (a duration string; the row fails), `schema` (or none for plain text), `prompt` or `messages`, and, for anything else WorkflowAgent takes, `agent` (constructor options: sampling, provider options, `prepareStep`, `prepareCall`, callbacks, telemetry, contexts) and `call` (per-call options: `toolChoice`, `activeTools`, transforms). Schemas use nullable fields, never optional ones, and no string formats (`z.string()`, not `.url()` or `.email()`); the description carries the intent. Never pass `timeout:` or `AbortSignal.timeout()` to the SDK; never build MCP clients or tools by hand. `example-research.ts` is the reference. - A single-shot AI step is `generateObject({ model: gtmModel(), ... })` inside a step; anything that uses tools is a `runAgent` stage. Workflow code never names a model id or reads `GTM_MODEL` itself: `lib/models.ts` is the only place model ids live. `gtmModel()` returns `GTM_MODEL` when the user chose a model, else the default that follows `GTM_AGENT_BACKEND`: `claude` gives the newest Claude Haiku, `codex` or anything else GPT-6 Luna, so the hosted copy stays with the author's local family. `setup --deploy` copies `GTM_AGENT_BACKEND` to Production, where it only picks that default. When the user names a model for the deployed copy, set `GTM_MODEL` on the Vercel project (Production), not in code; a per-stage `model` is only for a stage that truly needs a different model. The stage's backend is decided per run from the frozen environment: `GTM_AGENT_BACKEND` (`claude` or `codex`: the author's subscription, one step, this machine only; MCP servers and web tools carry over; codex needs each MCP server's `allow` list) or the Gateway when unset. A stage that uses custom tools, `approve`, `stream`, or `messages`, and every stage on the hosted copy, runs on the Gateway regardless. `backend` on a stage overrides the variable for that stage only; never write `backend` just to pick local or hosted, the variable does that. No other AI path exists. - Criteria: a workflow never copies an ICP or persona into its code (no title lists, size limits or rule text taken from the files). It reads them by slug when the run starts: `const icp = await readIcp("")` or `readPersona("")` from `lib/criteria.ts`, once in workflow scope before `runRows`, passing the result to the row step. Each returns `{ name, text, fields, signals, disqualifiers }`: `text` is the file for an AI step's prompt; `fields` holds each stated field by its label (`Unknown` ones absent), `items(fields["Job titles"])` splits a list; rule steps take titles, sizes, locations and disqualifiers from there. A rule step checks every stated field its rows can answer and names in the reason each stated field it could not check (a persona's Languages when rows carry no language), never skipping one silently. Local runs read the files each time; the build bakes them into `lib/criteria.generated.ts` (gitignored) for the deployed copy, so an ICP or persona edit reaches the next run there after its push deploys. Rows still inside `freshForMs` keep scores made under the old criteria. A missing file fails the run by name. - Doc comment: first line is the title, second paragraph the one-sentence summary; the diagram page falls back to the summary when `viewer.description` is missing. - Workflow description: on Create, Update and Upgrade write `viewer.description`, what the whole workflow does in plain language, about 280 characters and never over 500 (the build fails above 500); rewrite it when a change makes it untrue. See [viewer](references/viewer.md#write-the-workflow-description). - Viewer metadata: author literal `viewer.businessGraph` with stable node IDs, human labels, input/action/decision/output kinds, explanations and meaningful decision edges. Keep provider/caching details in explicit owner fields. Review the diagram against the actual workflow code on Create, Update and Upgrade; preserve `viewer.id`. See [viewer](references/viewer.md) for the schema and validation. - Where to look: follow [entry links](references/entry-links.md) after create/change, deploy/upgrade, ordinary runs, and explicit requests. Return the canonical private `viewerUrl` as **Open GTM Workflows**. This is separate from issuing an external share grant. - Connections: read the target inventory before writing a paid or AI step and in every pre-run check. Name a missing connection once and open the trusted Connections form; use its workflow-declared gateway relationship when deciding which key is required. Author literal `viewer.connections` alongside the business graph, one entry per key the code reads (exact variable), so the workflow's Connections tab can show each as set or missing. Follow [Connections](references/connections.md) for unknown keys, activation state and safe credential entry. - Gateways: a paid step that uses a provider found through a gateway tool the host exposes (a data marketplace, an MCP server) calls that gateway from code with the gateway's own key, `_API_KEY` managed through Connections, never the provider behind it. When the gateway's HTTP API is unknown, read its documentation before writing the step; do not guess. - Engine features are used natively (see local.md): a hook or `sleep` is a `wait` node, a child workflow a `sub` node, a `runAgent` stage an `agent` node. - Reaching people: `notify({ kind, text, approval?, target })` from `lib/notify.ts` posts to the GTM agent's notify route, which posts the text straight to Slack with the agent's bot token and no model call; `tell` and `show` are plain posts, `ask` and `handoff` open a message whose thread the agent watches, so a person's reply there wakes it. An approval in a stage notifies by itself when `GTM_AGENT_URL` and `GTM_NOTIFY_SECRET` are on the workflow project. Where it posts is code: `const NOTIFY = { channelId: "C0…" }` at the top of the workflow, given as `notify: input.notify ?? NOTIFY` to each stage and as `target` to each `notify` call; posts land top-level in that channel, never in the conversation a run was started from. Per-row news goes through `runRows`, never a `notify` call inside the step: `notify: { target: input.notify ?? NOTIFY, every: "chunk", line: (row, columns) => … }`, where `every` is `row` (one post per finished row), `chunk` (one post per run of rows, so one per child when fanned out; the default to propose) or `run` (one post with the totals at the end), a count and never a judgment; `line` returns a string, or `{ text, blocks }` when a row deserves a rich post. `notify: false` on a stage keeps its approvals silent. The agent project's `GTM_NOTIFY_CHANNEL` is only the fallback when a workflow names none. A workflow calls the agent only to reach people, never to think. - Helpers are admitted to `lib/` only when a bare SDK call was shown to fail or double-spend; everything else is a native SDK call with a worked example in [patterns](references/patterns.md). - Routes (hosted: behind Vercel Authentication, reached with `vercel curl` from a laptop or the host's bypass from the hosted agent; locally no password, this computer only, and changes never from another web site): `POST /api/run/`, `GET /api/run/` (Vercel Cron only, `CRON_SECRET`; refused locally), `POST /api/intake/` (no bearer; the sender's signature; senders use the share project's relay), `GET /api/runs/` (status, output, error, and `approvals`, pending first), `POST /api/runs//approve` with `{ token, approved, reason? }`, `GET /api/runs//stream` (one JSON event per line while an agent stage streams), `POST /api/runs//cancel`, `GET /api/link/`. Base: `http://localhost:3939` locally, `GTM_WORKFLOW_URL` for the deployed copy. When `GTM_AGENT_HOSTED` is `1`, the host adds Vercel's protection bypass to every call outside the agent's reach; send no credential. ## Outputs `workflows/.ts`, its table and migration, registry entries, a `vercel.json` cron when scheduled, a diagram page, rows in the result table, and, once a project is connected, the deployed copy that every push refreshes. For queue, provider, MCP, or CLI failures, read [runtime reliability](references/reliability.md#diagnose-the-failing-boundary) before attributing the cause or proposing a retry. ## Exceptions Requires the `gtm-workspace` skill installed alongside this one; when `../gtm-workspace/SKILL.md` is missing, say: install it the same way this skill was installed, with `npx skills add eliasstravik/gtm-skills -s gtm-workspace -y` (add `-g` when this skill lives in the global skills directory), then retry. A missing key stops a run before it starts and is named. A production build on Vercel with no `DATABASE_URL` fails on purpose; report it and say the database is added through the Neon integration, as [deploy.md](references/deploy.md#database) describes. A run on the deployed copy needs a live project (a linked `workflows/.vercel/project.json` on a computer; `GTM_WORKFLOW_URL` on the hosted agent); without one, say how to take it live as Deploy does (shared setup `--deploy` on a computer with the CLIs signed in; `gtm-agent` is optional) instead. A `workflows/` that contains `scripts/gtm.ts` is the previous runtime, which Upgrade does not touch: offer the migration in [updates](../gtm-workspace/references/updates.md#migrate) (set it aside, where the workspace's history keeps it and its deployed copy runs until the next push, then rebuild on the current runtime). ## QC - Every workflow has a valid business graph whose labels, branches and results match its actual code. Business behavior changes update metadata in the same change. The metadata build passes; compiler graphs never substitute for missing business metadata. - Every step is the plainest kind that does its job: no AI step for deterministic work, no agent stage where plain and AI steps in a fixed order would do, unless the user named an agent. - A one-row test was offered, through the question tool, before the first run of more than one row of a new or changed workflow; a one-row request ran without that question; the cost statement preceded every run. - The result table has the four fixed columns; paid and AI steps have `maxRetries = 0`; no `cached()` or `generateObject` call sits in workflow scope. - Every agent stage is a `runAgent()` call in workflow scope with `maxSteps`, `maxUsd`, and `timeout` set; no `WorkflowAgent` import, no `timeout:` option, no `AbortSignal.timeout`, no `.url()` or `.email()` in a schema, no optional fields in an output schema; a new or changed agent stage was tested on one row and the reply named its tool calls, stop reason, and real cost. - Every workflow passes `fanOut` with itself, `chunkSize` 20 when it has an agent stage; a module a workflow imports never reaches the database outside a `"use step"` function; route-only code lives in `*-api.ts` files. - An ordinary Run reply ends with the result and repeats no entry unless its address changed or the user explicitly requested it; a save ends with what was created, changed, or deleted. ## References [interactions](references/interactions.md) dialogues; [local and production](references/local-and-production.md) for the native commands that reach production; [imports](references/imports.md) for CSV imports, which are runs started with the file's rows; [local.md](references/local.md) for start, keys, data, schedules, tables, the viewer, the agent stage, fan-out, intake, notify, and the findings ledger; [patterns](references/patterns.md) for native SDK calls with worked examples; [deploy.md](references/deploy.md); `templates/workflows/example-scores.ts` as the reference implementation and `templates/workflows/example-research.ts` as the agent-stage reference; from gtm-workspace: [interaction](../gtm-workspace/references/interaction.md), [contract](../gtm-workspace/references/contract.md).