TL;DR: GTM engineering is learned by shipping small, testable builds, not by reading job descriptions. Do six in order with the firstsales CLI: a read-only status check, a safe import, a do-not-contact guard, a launch gate, a reply review queue and an alert. Each one ends with a test that either passes or fails.
#The point of view: skills are proven by tests, not titles
Anyone can put "GTM engineer" in a profile. What separates the people who can do the work is a small set of builds that run, fail loudly and can be handed to someone else.
So this post is a curriculum. Six builds, in a fixed order, each with an acceptance test written before you start.
The rule is simple: a build is finished when its test passes on a fresh machine, not when it looks right in a chat window. That rule is the unique angle here.
We care less about what a GTM engineer is called and more about what you can show running.
#How this differs from the role description
We already wrote about the job itself in the GTM engineer role. That post covers what the role owns, where it sits and why companies hire for it.
This one does not repeat any of that. It assumes you have decided to do the work and asks what you should build first, in what order and how you will know each build is sound.
If you want the role context, read the first post and come back. If you want to start building today, stay here.
#Why a curriculum helps
Outbound work punishes untested changes. A bad import creates duplicate contacts, a bad filter emails someone who opted out and a bad launch burns a domain.
Small builds with tests keep the cost of each mistake low while you learn. They also give you a portfolio of working tools, which is better evidence than a certificate.
#What you need before you start
You need a terminal, Node.js, a text editor and a FirstSales workspace you are allowed to experiment in. You also need Claude Code if you want an assistant to draft scripts and read output with you.
You do not need to be a senior engineer. You need to be comfortable reading JSON, running shell commands and saying "I do not know yet" until a test tells you.
#The skills underneath every build
Each build practices the same handful of skills. Name them, because you will keep reusing them.
| Skill | What it looks like in practice | Where it shows up |
|---|---|---|
| Reading an API contract | Using the command registry and OpenAPI file, not guesses | Every build |
| Shell and JSON | jq, exit codes, files in git | Builds 1, 2, 4 |
| Idempotency | Safe retries with a key per record | Builds 2, 3 |
| Dry runs | Previewing a change before it happens | Builds 2, 3, 4 |
| Guard design | A check that stops a send, not one that reports it | Builds 3, 4 |
| Human review design | A queue where a person approves what matters | Build 5 |
| Alert design | A message with context and one next action | Build 6 |
#Reading the contract
The FirstSales CLI is a thin client over the public /api/v1 Developer API, per its README. The command registry is the source of truth for what exists, and firstsales commands --json prints it.
Get in the habit of checking the registry before you ask an assistant to write a command. An invented flag looks plausible and fails at the worst moment.
#Safe by default
The CLI ships with habits that match good engineering. Mutations can take --idempotency-key, previews use --dry-run, and destructive commands need --confirm.
The recommended agent loop in the README is the loop you should practice by hand first: confirm identity, read state, make the smallest change, preview, add an idempotency key, confirm only if destructive, then re-read to verify.
#Why Claude Code skills matter here
Claude Code supports skills, folders with a SKILL.md that teach Claude a procedure, as the Claude Code skills documentation explains. The FirstSales package ships three skills named firstsales-cli, firstsales-product-mcp and firstsales-developer-api.
Writing your own skill for a repeatable job, such as your launch gate, is one of the most useful things a GTM engineer can do. It turns a habit into something a teammate can run.
#Set up the workbench
Install the CLI and check your setup before any build.
bashnpm i -g @firstsales.io/cli export FIRSTSALES_API_KEY="<YOUR_FIRSTSALES_API_KEY>" export FIRSTSALES_BASE_URL="https://api.app.firstsales.io" firstsales whoami --json firstsales doctor --pretty
Create a dedicated key for your experiments with firstsales api-keys create, using the least access that covers the build. Read API keys for outbound agents for the reasoning.
Never paste an API key into a prompt or a post. Keep it in an environment variable and out of git.
#A repository for your builds
Make one repository, with one folder per build. Each folder holds a script, a short README and a test file.
gtm-builds/ 01-status-check/ 02-safe-import/ 03-dnc-guard/ 04-launch-gate/ 05-reply-queue/ 06-alert/ shared/ids.env
Put the organization and workspace ids in shared/ids.env so every script reads the same values. Find them with firstsales orgs list --json and firstsales workspaces list --org org_123 --json.
#The test rule
Each build gets a test.sh that exits 0 on pass and non-zero on fail. Write it first, watch it fail, then write the build.
The CLI exit codes help here: 0 is success, 1 a runtime error, 2 a usage error, 3 an auth error, 4 not found and 5 rate limited. A test can assert on them directly.
#Build 1: a read-only status check
The first build changes nothing. It answers one question: is my workspace in a state where I would run anything?
#What it does
The script runs whoami, lists campaigns and prints one line per campaign with its state and progress.
bashfirstsales whoami --json firstsales campaigns list --org org_123 --workspace ws_123 --json firstsales campaigns progress --org org_123 --workspace ws_123 --campaign camp_123 --json
Pipe the JSON through jq to pick the fields you care about. Check the real field names in the output on your own workspace, because this post does not assume them.
#Acceptance test
The test passes when all of these hold.
| Check | Pass | Fail |
|---|---|---|
| Script exits 0 on a valid key | ✓ | ✗ |
| Script exits 3 with a bad key and prints a clear message | ✓ | ✗ |
| Output has one line per campaign | ✓ | ✗ |
| Script performs no write call | ✓ | ✗ |
| Script prints nothing secret | ✓ | ✗ |
The "no write call" check is the one beginners forget. Run the script with --dry-run semantics in mind: if you add any mutating command by mistake, the test should catch it by scanning the script for write verbs.
#What you learn
You learn the shape of the data, the exit codes and how scope limits your view. Try the script with a key that has narrow access and see which calls fail.
That failure is information. It tells you what a least-access key can and cannot do, which matters for every later build.
#Build 2: a safe, repeatable import
Imports are where most outbound mistakes start. A repeatable import is the first build that mutates data, so it is the first that can hurt you.
#What it does
The script takes a CSV of name,domain rows and loads companies. The README gives a jq recipe for turning that CSV into the body the API expects, and a companies import command that takes it.
bashjq -R -s '{companies: (split("\n") | .[1:] | map(select(length > 0) | split(",") | {name: .[0], domain: .[1]}))}' companies.csv > companies.json firstsales companies import --data-file companies.json \ --org org_123 --workspace ws_123 --dry-run firstsales companies import --data-file companies.json \ --org org_123 --workspace ws_123 --idempotency-key import-2026-10-08-001
The README says the body holds 1 to 10,000 rows and that the API enforces the limit. Preview first, then run with an idempotency key.
#Contacts one at a time
For contacts, the documented first mutation uses contacts create with a body and a per-row key.
bashfirstsales contacts create \ --org org_123 --workspace ws_123 \ --data '{"email":"[email protected]","firstName":"Alex"}' \ --idempotency-key import-row-001 --json
Use a stable key built from the row, such as the email, so a rerun does not create a second record. The README shows exactly this pattern with --idempotency-key [email protected].
#Acceptance test
Run the import twice with the same keys. A pass means the second run creates nothing new.
| Check | Pass | Fail |
|---|---|---|
| First run loads all rows | ✓ | ✗ |
| Second run with same keys adds zero records | ✓ | ✗ |
| A malformed row is reported, not skipped silently | ✓ | ✗ |
--dry-run output matches what the real run does | ✓ | ✗ |
| Row count in equals row count loaded plus rejected | ✓ | ✗ |
The last check is a reconciliation. If 500 rows went in and 480 loaded, you must be able to say where the other 20 went.
#Where this connects
The wider list-building workflow, including how to source and shape lists, is in import leads and build lists from the terminal. Data quality decays quickly, which B2B data decay and list hygiene covers, so build the reconciliation habit now.
#Build 3: a do-not-contact guard
This build matters more than any other, because it protects people who said no. It also protects your domain.
#What it does
The script checks a list of addresses and domains against suppression and your blocked domains before anything is loaded or sent.
bashfirstsales suppression check [email protected] acme.com \ --org org_123 --workspace ws_123 firstsales blocked-domains list --org org_123 --workspace ws_123 --json firstsales blocked-domains add example-competitor.com \ --org org_123 --workspace ws_123 --idempotency-key block-001
The README says suppression check takes up to 100 named addresses or domains and returns value, suppressed and reason for each. It also says there is no list or export, so your script must batch in groups of 100.
#Design it as a guard, not a report
A report says "12 of your contacts are suppressed." A guard removes those 12 and stops if it cannot check.
The difference is the failure mode. If the check call fails, a report prints nothing and the pipeline continues.
A guard exits non-zero and the pipeline stops.
#Why this is more than courtesy
The FTC's CAN-SPAM compliance guide for business states that the rules apply to all commercial email, including business-to-business messages, and that you must honor opt-out requests promptly. A guard is how you make that true in code.
Mailbox providers care as well. Google's email sender guidelines ask bulk senders to support easy unsubscribe and to keep spam complaint rates low, and RFC 8058 defines the one-click unsubscribe headers that make this work.
Ignoring a no is both a legal and a deliverability problem.
#Acceptance test
| Check | Pass | Fail |
|---|---|---|
| Known suppressed address is removed from the output | ✓ | ✗ |
| Script exits non-zero if the check call errors | ✓ | ✗ |
| Lists over 100 values are batched correctly | ✓ | ✗ |
| Domain-level block removes every address on that domain | ✓ | ✗ |
| Output file contains only cleared contacts | ✓ | ✗ |
Test the failure path on purpose. Set a bad base URL and confirm the script stops instead of passing everything through.
#Related reading
The longer treatment of lists and exclusions is in do-not-contact lists through MCP, and the policy side is in is cold email legal in 2026.
#Build 4: a launch gate that can say no
The fourth build is the one that earns trust from a team. It decides whether a campaign is allowed to start.
#What it does
The gate runs the readiness check and a test send, then reports pass or fail. It never starts the campaign by itself.
bashfirstsales campaigns readiness --org org_123 --workspace ws_123 --campaign camp_123 --json firstsales campaigns prepare-test-email --org org_123 --workspace ws_123 --campaign camp_123 --json firstsales campaigns send-test-email --org org_123 --workspace ws_123 --campaign camp_123 --data-file test-email.json --json
Those three come from the CLI's command list (firstsales commands --json). The body fields for the test email come from the public contract, so read the schema before writing the file.
The safety line applies without exception: run campaigns readiness and campaigns send-test-email before campaigns start.
#Add domain checks
A good gate also looks at the sending foundation. The CLI offers email-auth status, email-auth verify, warmup status and tracking-domains verify.
bashfirstsales email-auth verify --domain acme.com --dkim-selector s1 \ --org org_123 --workspace ws_123 --json firstsales warmup status --org org_123 --workspace ws_123 --json firstsales tracking-domains list --org org_123 --workspace ws_123 --json
Authentication is not optional anymore at scale. Google's sender guidelines require SPF, DKIM and DMARC for bulk senders, and Microsoft announced similar requirements for high-volume senders to Outlook.com in its Defender for Office 365 blog.
See the pre-launch checklist from the terminal for the full list of checks, and the cold email deliverability checklist for the wider picture.
#The gate must be able to say no
A gate that always passes is decoration. Prove yours can fail by feeding it a campaign that should not launch.
Use a draft campaign with a missing step, or a domain that has not been verified. The gate should exit non-zero and print the reason in plain words.
#Who starts the campaign
Starting is a separate, human action. The campaigns start body needs a savedVersionId, a readinessVersion, an idempotencyKey and a confirmation, per the CLI README.
That design makes the human step explicit. Your gate produces the evidence, and a person supplies the confirmation.
The guide on starting, pausing and resuming campaigns from the CLI shows the full sequence.
#Acceptance test
| Check | Pass | Fail |
|---|---|---|
| Gate fails on an unverified sending domain | ✓ | ✗ |
| Gate fails if the test send errors | ✓ | ✗ |
| Gate passes on a campaign you know is sound | ✓ | ✗ |
Gate never calls campaigns start | ✓ | ✗ |
| Output names the failing check in one line | ✓ | ✗ |
Make the last check strict. A gate whose failure message says "something went wrong" will be bypassed within a month.
#Build 5: a reply review queue
Replies are where outbound meets people. This build teaches you human review design.
#What it does
The script pulls inbox threads, lists those waiting on a draft and shows each draft next to the original message for approval or rejection.
bashfirstsales inbox threads --org org_123 --workspace ws_123 --json firstsales inbox thread --org org_123 --workspace ws_123 --thread thread_123 --json firstsales inbox approve-draft --org org_123 --workspace ws_123 \ --email email_123 --idempotency-key approve-email-123 firstsales inbox reject-draft --org org_123 --workspace ws_123 --email email_123 --json
The README describes these as the commands for reply monitoring, draft approval queues, human-in-the-loop workflows and read-state sync. It also notes that inbox score and inbox ai-draft request scoring or a draft and do not send an email.
#The design problem
A review queue fails in two ways. It either shows too much, so nobody reads it, or it shows too little, so approvals are blind.
Show the incoming message, the draft, the reason the draft was produced and one clear action. Keep the queue short by sorting on what needs a decision today.
#Why a person stays in the loop
Buyers use AI during their own research, and they trust people to check it. A Gartner release from May 2026 reports that 69% of B2B buyers turn to sales reps to validate AI-generated insights (Gartner press release).
A reply is often the moment a buyer checks what they found against a real person. A wrong or tone-deaf automated reply costs more there than almost anywhere else.
Our posts on sorting your reply inbox from Claude Code and the reply handling playbook go deeper on categories and responses. For the failure mode to design against, read AI reply misclassification.
#Acceptance test
| Check | Pass | Fail |
|---|---|---|
| Queue shows only threads that need a decision | ✓ | ✗ |
| Approve uses an idempotency key, so a double click sends once | ✓ | ✗ |
| Reject leaves the thread open for a human reply | ✓ | ✗ |
| Queue lists the original message beside each draft | ✓ | ✗ |
| Nothing sends without an explicit approve action | ✓ | ✗ |
The fifth check is the one that matters. Test it by running the script and doing nothing, then confirm no email left.
#Build 6: an alert that tells a human something useful
The last build is small and teaches the most about judgment. An alert that fires too often gets muted, and an alert that says nothing useful gets ignored.
#What it does
The script reads open alerts and posts a short summary to a channel you control. The CLI has alerts list, alerts ack and alerts resolve.
bashfirstsales alerts list --org org_123 --workspace ws_123 --json firstsales alerts ack --org org_123 --workspace ws_123 --alert alert_123
Check the registry for the exact flags before you script ack, because this post does not assume them.
#Add a message with context
For delivery to a team channel, Slack documents incoming webhooks that accept a JSON payload posted to a URL. Treat that URL like a secret, because anyone who has it can post to your channel.
A useful alert has four parts: what happened, how big it is, what to check first and who owns it. Anything less sends someone hunting for context.
#The cooldown rule
Add a cooldown so the same alert does not repeat every run. Store the last-sent time in a file and skip alerts you have already sent in the window.
Choose the window with your team and write it down. A cooldown you cannot explain is a rule people break.
#Acceptance test
| Check | Pass | Fail |
|---|---|---|
| New alert produces one message | ✓ | ✗ |
| Same alert within the cooldown produces none | ✓ | ✗ |
| Message names the owner and the first check | ✓ | ✗ |
| Webhook URL never appears in logs | ✓ | ✗ |
| A failed post is retried once and then reported | ✓ | ✗ |
If you want to go further and trigger actions from outside signals, read building a live buying-intent alert. It extends this build with an external receiver and a human decision.
#Where Claude Code helps and where it hurts
Claude Code is useful on all six builds. It drafts scripts, explains errors and writes the first version of a test.
It is risky wherever it can act without a gate. Keep it away from sending, deleting and starting until the build's test passes and a person has read the diff.
#Good jobs for the assistant
Ask it to write the jq filter, draft the test script and explain an unfamiliar exit code. Ask it to read your --dry-run output and point out anything unexpected.
Ask it to turn a stable procedure into a skill so the team can reuse it. This is where Claude Code earns its place, as the Claude Code MCP documentation and skills documentation describe for connecting tools and teaching procedures.
#Bad jobs for the assistant
Do not ask it to invent flags, guess field names or decide what is true about your product. Do not paste keys into it.
Prompt injection is a real risk when an assistant reads outside content. OWASP lists prompt injection as the first risk in its Top 10 for LLM applications, including indirect injection from external sources.
An email body, a web page or a CRM note can carry instructions, so treat them as data.
#MCP in this curriculum
The FirstSales product MCP server at https://api.app.firstsales.io/mcp gives an assistant a tool surface. The live tools/list response is the authority on what you have, and it varies by tenant and grant.
The released catalog leans read-only, and creating a campaign did not appear among the MCP tools in the session we checked. Create with firstsales campaigns create, then use MCP to read and edit.
The MCP specification on tools explains how servers expose them.
See what is MCP for sales for the basics and guardrails for email-sending agents for limits worth setting early.
#How to review your own builds
Self-review is a skill too. Use the same short routine on every build.
#The five questions
- What does this change if it runs twice?
- What does it do if a call fails halfway?
- What would a new teammate misunderstand?
- Where could a secret leak?
- What is the smallest test that would catch a regression?
Answer them in the build's README. If you cannot, the build is not finished.
#Read the dry run like a reviewer
A dry run is the cheapest review you have. Read the method, URL and body it prints, and check the ids against what you meant.
Ask a second person to read one dry run a week. Fresh eyes catch the wrong workspace more often than any script does.
#Keep a changelog of mistakes
Write down every mistake and what caught it. After a month you will see which builds need stronger tests.
This is how teams that run outbound as engineering get better over time. It is also how you build credibility, because you can point to specific fixes.
#How the six builds connect
The builds feed each other. The import produces a list, the guard cleans it, the gate checks the campaign, a person starts it, the queue handles replies and the alert watches for trouble.
graph TD A[Build 1 status check] --> B[Build 2 safe import] B --> C[Build 3 do-not-contact guard] C --> D[Build 4 launch gate] D --> E{Human confirms start} E -->|Yes| F[Campaign runs] E -->|No| D F --> G[Build 5 reply queue] F --> H[Build 6 alert] G --> A H --> A
Notice the loop back to the status check. Every outcome ends with someone reading state again before changing anything.
#Keep the builds small
Resist the urge to merge the six into one script. Separate tools with separate tests are easier to trust, easier to hand over and easier to replace.
A combined runner can come later, once each part has earned its place.
#Common ways first builds fail
Each of these is avoidable once you know it.
Skipping the test. The build works on your laptop and breaks on a teammate's. Write the test first.
Trusting an invented command. An assistant produces a flag that does not exist. Check the registry.
No idempotency. A retry creates duplicates. Add a key per record.
A guard that reports. The check prints a warning and the pipeline continues. Make it exit non-zero.
Alert fatigue. Every run posts a message, so nobody reads them. Add a cooldown and context.
Treating the model as an authority. It sounds sure, so you trust it. Check it against the contract and your data.
#The wider system
These builds are parts of a system. The five layers of an AI outbound agent stack shows where each fits, and running outbound from the terminal shows them combined.
If you are weighing tools, CLI, MCP or the app gives a decision rule. And where FirstSales fits in the outbound stack explains the product's place.
#A realistic learning order
The order matters more than the speed. Each build uses what the last one taught.
#Week-sized chunks
Treat each build as one focused block of work, and do not start the next until the test passes. Two builds in one sitting is fine if the tests pass.
We are not promising a calendar. How long it takes depends on your background and how often you can work on it.
#What to do after the six
Pick a real problem from your own outbound. Maybe it is lists that arrive messy, replies that wait too long or launches that surprise people.
Build the smallest tool that fixes it, with a test, and add it to the repository. That is the habit that matters.
#Where to go next
For deeper work on signals and triggers, read buying signals for cold email and compound buying signals. For measurement, sales funnel metrics and cold email benchmarks give you reference points.
#FAQ
#What is GTM engineering?
It is the practice of building and running go-to-market systems with code, data and automation. This post treats it as a set of testable builds rather than a job title.
#How is this different from your GTM engineer role post?
The role post explains what the job owns and why teams hire for it. This one is a build curriculum with acceptance tests, and it assumes you have decided to do the work.
#Do I need to be a developer?
You need to be comfortable in a terminal, reading JSON and writing short scripts. A senior engineering background is not required, but patience with small, exact details is.
#What should I build first?
A read-only status check. It changes nothing, teaches you the data shape and exit codes, and gives you a safe first test.
#Why write the test before the build?
It forces you to decide what done means. It also gives you a fast way to prove the build still works after you change it.
#How do I stop an import from creating duplicates?
Use a stable idempotency key per record, such as the email address, and run the import twice in testing. The second run should add nothing.
#What does --dry-run do?
It prints the request without sending it, including the method, URL, redacted headers and body. It is the cheapest review step you have.
#Can Claude Code write these builds for me?
It can draft scripts, tests and explanations. You still need to check commands against the registry and keep it away from sending and deleting until a test passes.
#What if Claude invents a flag?
Check firstsales commands --json and the README. If the flag is not there, do not use it.
#How do I keep API keys safe?
Use environment variables, give each key the least access it needs and never paste one into a prompt or a post. Create keys with firstsales api-keys create and revoke old ones with --confirm.
#What is the safe way to launch a campaign?
Run campaigns readiness and campaigns send-test-email first, then have a person confirm the start. Never launch from a script that has not passed both.
#Can MCP create campaigns?
Not that we confirmed. Create with firstsales campaigns create, then use MCP to read and edit.
Check the live tools/list for your own tenant.
#Why does the do-not-contact guard exit non-zero on error?
Because a silent failure lets a send go to someone who opted out. A stopped pipeline is annoying, and a wrong send is a legal and reputation problem.
#How many contacts can I check at once?
suppression check takes up to 100 addresses or domains per call, per the README. Batch larger lists.
#What are the CLI exit codes?
0 is success, 1 is a runtime error, 2 is a usage error, 3 is an auth error, 4 is not found and 5 is rate limited. Your tests can assert on them.
#Why keep a human in the reply queue?
Buyers check AI-generated information against people, as Gartner's May 2026 survey release reports. A wrong automated reply at that moment costs more than a slow one.
#How often should an alert fire?
Once per real event, with a cooldown to prevent repeats. Pick the window with your team and write it down.
#What should I do after the six builds?
Pick one real problem in your outbound and build the smallest tool that fixes it, with a test. Add it to your repository.
#Is a Claude Code skill worth writing?
Yes, for any procedure you repeat. A skill turns your launch gate or import routine into something a teammate can run the same way.
#Where do I see a full end-to-end example?
Read building a cold email campaign from Claude Code for creation, and running outbound from the terminal for the combined workflow.
#Conclusion
GTM engineering is a practice you show with working tools. Six small builds, each with a pass or fail test, give you that proof.
The short version:
- Write the test first and let it fail.
- Start read-only, then import safely with idempotency keys.
- Build the do-not-contact guard so it stops pipelines instead of only reporting.
- Make the launch gate able to say no, and keep the start action human.
- Design the reply queue so nothing sends without approval.
- Keep alerts rare, specific and owned.
Start with firstsales whoami --json, then build the status check. Next, read the GTM engineer role if you want the career context, and building a cold email campaign from Claude Code when you are ready for the first real campaign.
Never paste an API key into a prompt or a post, and remember that delete commands cannot be undone.