gtm-workflow
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).
How do I install this agent skill?
npx skills add https://github.com/eliasstravik/gtm-skills --skill gtm-workflowIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The gtm-workflow skill is a legitimate framework for managing and deploying Go-To-Market workflows to Vercel and local environments. It facilitates data enrichment through external providers like Monid and Blitz, handles local Postgres database management via the embedded-postgres package, and provides a web UI for workflow monitoring. Security is maintained through robust SSRF protection in its web fetcher, secure handling of secrets using environment variables, and the use of safe process spawning patterns to prevent command injection.
- Socketwarn
2 alerts: gptAnomaly
- Snykwarn
Risk: unknown · No issues
What does this agent skill do?
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 (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; 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 <workspace>/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. The remaining workspace files outside workflows/ belong to the other gtm skills.
Inputs
The request; the workspace found by the contract; 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), 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 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 says. Talk by the six rules in interaction; reproduce the dialogues. 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, 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, with labels and branches checked against the new code. Write workflows/<slug>.ts from example-scores.ts, or from example-research.ts when it has an agent stage, its table in db/tables/<name>.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 <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/<slug> on the deployed copy every 10 seconds, up to 3 minutes, until git merge-base --is-ancestor HEAD <commit> 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. |
| 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, 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. Resolve the discussed workflow by stable identity; without one, open the list. Check readiness. Locally start npm run dev if needed; its Connections tab (<viewer origin>/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=<id>&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. |
| Deploy | Deploy is the push; follow 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 <workflows> 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 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 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 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. |
| 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. 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;
localhostnever appears in a reply. - Keys: read
GET /api/connectionsthrough 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. Direct missing-key entry to the verifiedconnectionsUrlfrom 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 defines the optional
dataregistry 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. Preserveviewer,data, andintakeduring upgrades. Sharing is an explicit private-viewer action; it requires a versioned table/column/relationship/row policy described in viewer. -
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,generateObjectwith 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 })fromlib/intake.ts, registered asintakeinworkflows/index.ts;POST /api/intake/<slug>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); never from a workspace file or local-only data. Input isRowsInputfromlib/rows.ts,{ rows?, maxRows?, maxSpendUsd? }, each row an object with a required stringkey, the table's primary key; the workflow's constants are the defaults; the start route merges a POST body overdefaultInput, so the limited run isPOST { maxRows: 1 }. -
runRows({ rows, table, step, maxRows, maxSpendUsd, estimateUsd, freshForMs, concurrency?, saveEvery?, fanOut? })fromlib/rows.tsis 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 throughdb/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 passesfanOut: { workflow: <its own function>, 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.chunkSizeis 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, everycached()andgenerateObjectcall happen inside"use step"functions declared asasync function name() { "use step"; }, never arrow or method form; every paid or AI step setsname.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 }), neverselect()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 inlib/read-budgets.ts;tests/read-budgets.test.tsin this skill enforces them, and local says how to measure a workflow's own reads before shipping it. -
Agent stages: an agent inside a workflow is always
runAgent()fromlib/agent.ts, called from a plain async function in workflow scope (the stage function, anagentnode), neverWorkflowAgentdirectly and never inside a"use step"function; the helper makes every model call and tool call a step, races the time limit againstsleep(), cleans the schemas, and returns real cost. Its config is the whole authoring surface:modelandreasoning(leavemodelout: it defaults togtmModel()fromlib/models.ts, see the next rule; reasoning defaults toGTM_REASONING, elsehigh),instructions,skills(names fromskills/index.ts, each a text module underskills/),tools(mcpservers by name withurl,keyEnv,allow,maxCalls;web.fetchfree, a step;web.searchexecuted by AI Gateway (trueor"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;customtools whoseexecuteis 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 untilPOST /api/runs/<id>/approvedecides it; settimeoutin days when approvals may wait),stream(every model and tool event onGET /api/runs/<id>/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),promptormessages, and, for anything else WorkflowAgent takes,agent(constructor options: sampling, provider options,prepareStep,prepareCall, callbacks, telemetry, contexts) andcall(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 passtimeout:orAbortSignal.timeout()to the SDK; never build MCP clients or tools by hand.example-research.tsis the reference. -
A single-shot AI step is
generateObject({ model: gtmModel(), ... })inside a step; anything that uses tools is arunAgentstage. Workflow code never names a model id or readsGTM_MODELitself:lib/models.tsis the only place model ids live.gtmModel()returnsGTM_MODELwhen the user chose a model, else the default that followsGTM_AGENT_BACKEND:claudegives the newest Claude Haiku,codexor anything else GPT-6 Luna, so the hosted copy stays with the author's local family.setup --deploycopiesGTM_AGENT_BACKENDto Production, where it only picks that default. When the user names a model for the deployed copy, setGTM_MODELon the Vercel project (Production), not in code; a per-stagemodelis 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(claudeorcodex: the author's subscription, one step, this machine only; MCP servers and web tools carry over; codex needs each MCP server'sallowlist) or the Gateway when unset. A stage that uses custom tools,approve,stream, ormessages, and every stage on the hosted copy, runs on the Gateway regardless.backendon a stage overrides the variable for that stage only; never writebackendjust 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("<slug>")orreadPersona("<slug>")fromlib/criteria.ts, once in workflow scope beforerunRows, passing the result to the row step. Each returns{ name, text, fields, signals, disqualifiers }:textis the file for an AI step's prompt;fieldsholds each stated field by its label (Unknownones 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 intolib/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 insidefreshForMskeep 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.descriptionis 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. -
Viewer metadata: author literal
viewer.businessGraphwith 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; preserveviewer.id. See viewer for the schema and validation. -
Where to look: follow entry links after create/change, deploy/upgrade, ordinary runs, and explicit requests. Return the canonical private
viewerUrlas 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.connectionsalongside 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 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,
<GATEWAY>_API_KEYmanaged 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
sleepis awaitnode, a child workflow asubnode, arunAgentstage anagentnode. -
Reaching people:
notify({ kind, text, approval?, target })fromlib/notify.tsposts to the GTM agent's notify route, which posts the text straight to Slack with the agent's bot token and no model call;tellandshoware plain posts,askandhandoffopen a message whose thread the agent watches, so a person's reply there wakes it. An approval in a stage notifies by itself whenGTM_AGENT_URLandGTM_NOTIFY_SECRETare on the workflow project. Where it posts is code:const NOTIFY = { channelId: "C0…" }at the top of the workflow, given asnotify: input.notify ?? NOTIFYto each stage and astargetto eachnotifycall; posts land top-level in that channel, never in the conversation a run was started from. Per-row news goes throughrunRows, never anotifycall inside the step:notify: { target: input.notify ?? NOTIFY, every: "chunk", line: (row, columns) => … }, whereeveryisrow(one post per finished row),chunk(one post per run of rows, so one per child when fanned out; the default to propose) orrun(one post with the totals at the end), a count and never a judgment;linereturns a string, or{ text, blocks }when a row deserves a rich post.notify: falseon a stage keeps its approvals silent. The agent project'sGTM_NOTIFY_CHANNELis 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. -
Routes (hosted: behind Vercel Authentication, reached with
vercel curlfrom 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/<slug>,GET /api/run/<slug>(Vercel Cron only,CRON_SECRET; refused locally),POST /api/intake/<slug>(no bearer; the sender's signature; senders use the share project's relay),GET /api/runs/<id>(status, output, error, andapprovals, pending first),POST /api/runs/<id>/approvewith{ token, approved, reason? },GET /api/runs/<id>/stream(one JSON event per line while an agent stage streams),POST /api/runs/<id>/cancel,GET /api/link/<slug>. Base:http://localhost:3939locally,GTM_WORKFLOW_URLfor the deployed copy. WhenGTM_AGENT_HOSTEDis1, the host adds Vercel's protection bypass to every call outside the agent's reach; send no credential.
Outputs
workflows/<slug>.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 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 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 (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; nocached()orgenerateObjectcall sits in workflow scope. - Every agent stage is a
runAgent()call in workflow scope withmaxSteps,maxUsd, andtimeoutset; noWorkflowAgentimport, notimeout:option, noAbortSignal.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
fanOutwith itself,chunkSize20 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.tsfiles. - 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 dialogues; local and production for the native commands that reach production; imports for CSV imports, which are runs started with the file's rows; local.md for start, keys, data, schedules, tables, the viewer, the agent stage, fan-out, intake, notify, and the findings ledger; patterns for native SDK calls with worked examples; 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, contract.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/eliasstravik/gtm-skills/gtm-workflow">View gtm-workflow on skillZs</a>