Compare
Stampdrill and Bruno
Bruno made the same bet about files: requests belong in a folder in your repository, not in a workspace you sign into. If that part is already settled for you, the choice between the two is narrower than a feature list suggests. What differs is what one file can say, what a run does with it, and which machines you need it on.
Where the two agree
A Bruno collection is a folder of files on disk. So is a Stampdrill workspace: every .stamp file under it holds requests, and an environment.stamp at the root holds the values they share. In both cases the folder goes into git next to the code it calls, a change to a request arrives as a diff in a pull request, and a clone is a working set of requests. Neither needs an account to run what is in the folder, and both have a command line tool, so the same files run in a terminal and in a build pipeline. None of that separates them.
What one file can say
A .bru file is one request, written as named blocks: meta with its name and its place in the order, a block named after the method holding the URL, then headers, a body block such as body:json, an auth: block, and an assert block of checks. Past a check, the logic is JavaScript, in script:pre-request, script:post-response or tests. Environments are separate .bru files under environments/.
A .stamp file holds as many requests as you like, separated by a ### line, and the work around them is part of the same small language rather than an escape into JavaScript. This is one whole file:
plan Checkout {
tags smoke
step "Log in, then list" {
run logIn
expect status == 200
run listOrders
expect all(body.items, order => order.total > 0)
}
}
load Orders {
users 50
ramp 30s
duration 5m
threshold p95 < 800ms
threshold errors < 1%
scenario {
run listOrders
expect status == 200
}
}
### Log in
POST {{baseUrl}}/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
> assert status == 200
> set token = body.accessToken
### List orders
@needs logIn
@auth bearer {{token}}
GET {{baseUrl}}/orders?limit={{limit}}
> assert status == 200
> assert body.items.length <= limit
Four things in there are not the request itself:
@needs logInrecords that listing orders depends on a login. It runs once per session, whichever way the request is reached, sostamp run orders.stamp listOrdersworks on its own and does not rely on anyone running the file top to bottom.> assertand> setare the response script: a check that passes or fails, and a value kept for later requests in the session.savedoes the same and also writes the value toenvironment.local.stamp. The expressions are a small language withcontains,matches, lambdas andmap,filter,all,count,sum, and your ownfndefinitions.plan Checkoutis a multi step test in the same file as the requests it calls: steps, loops, conditions,retry,timeout, amatrixof environments anddata ./fixtures/carts.csvto run once per row.load Ordersis a load test with virtual users, a ramp, think time and thresholds. The thresholds decide the exit code, so a pipeline gates on p95 and error rate without anything else installed.
Environments differ in shape as well. Instead of one name picked from a flat list, a workspace declares an axis per thing that varies, and variable sets say which values they apply to:
dimension env = test, staging, prod
dimension region = latam, mena, apac
vars {
limit = 25
}
vars env=staging|prod {
host = "api." + env + "." + region + ".example.com"
}
vars env=test {
host = localhost(8080)
limit = 5
}
Then stamp run . env=staging region=mena picks a combination rather than a file, and stamp env . prints what every name is worth and which vars block decided it. Variables and dimensions covers it in full.
What a run can do
A race condition is a test, not a different product. concurrently N starts N actors at one instant, sync holds them together until the last one arrives, and results afterwards is each actor's last response, so the thing you want to assert, that exactly one withdrawal succeeded, is one line:
plan DoubleSpend {
setup { run logIn }
concurrently 10 {
sync "ready"
run withdraw with amount = 100
}
expect count(results, r => r.status == 200) == 1, "only one withdrawal may succeed"
}
The same files reach past HTTP. WS opens a socket and scripts the conversation with send and receive, GRAPHQL sends an operation with its variables after a blank line, STUN and TURN check the ICE servers a WebRTC call needs, and MCP connects to a Model Context Protocol server the way a client would, lists its tools, resources and prompts, and calls them with call, read and getPrompt. All of it is in the example workspace that ships with the repository, which stamp check counts as 18 files and 58 requests: Plans.stamp, Load and races.stamp, Playground/WebSockets.stamp, Playground/GraphQL.stamp, Playground/MCP.stamp and Playground/Mock data.stamp for repeatable sample data from a seed.
How much of that Bruno covers is a question for Bruno's own documentation, which moves. This page does not answer it on their behalf.
What Bruno has that Stampdrill does not
Three things, and any one of them can settle it.
- It runs on Windows and Linux as well as macOS. The Stampdrill app is a native Mac app and there is no build for anything else. The free
stampcommand runs on macOS and Linux, with static Linux binaries that need nothing installed, and there is no Windows build of that either. - The client itself is open source. For Stampdrill only part of it is. The engine and the command line tool are published at github.com/stampdrill/stampdrill under the Mozilla Public License 2.0, with the language reference, examples, agent skill and editor grammar under MIT. The Mac app is not in that repository and is not open source. If you want to read or change the window you are looking at, Bruno is the one that lets you.
- The client is free. Downloading and running Bruno costs nothing, and that is a real advantage at the front door. The Stampdrill app is $49.99 on the Mac App Store, bought once, and the command line tool is free, including commercially and in CI, so a build server and a colleague who works in the terminal cost nothing on either side. Past the free tier the two are priced differently: Bruno's paid plans are per-seat subscriptions, listed on usebruno.com/pricing at the time of writing as Pro, $6 per user a month billed annually, and Ultimate, $11, both described there as adding deeper git integration, automation and support. Stampdrill has one price and no tiers: every feature on this page is in the app you buy once.
One more limit worth knowing before you buy: the App Store app is sandboxed, so it cannot start another program. MCP servers launched with stdio: run from the command line tool, and the app connects to servers over HTTP.
Moving a collection across
Point the importer at the collection folder, the one with bruno.json in it, and it writes .stamp files. Bruno's YAML collections work the same way: a folder with opencollection.yml, or the bundled single file. A lone .bru file is also accepted.
stamp import ./my-collection -o api
stamp check api
It prints a line per file written, then the collection's name with how many requests landed in how many files, and a note line for anything it could not carry over as it was. Nothing is sent, and it never writes over a file that already exists: it stops and asks for another folder. In the app the same thing is File ▸ Import… (⇧⌘I).
What arrives:
- A folder of requests becomes one
.stampfile, nested folders become folders, and the order from each request'sseqis kept. - Headers, bodies (JSON, text, XML, SPARQL, URL-encoded and multipart forms with their file attachments, GraphQL with its variables) and the
docsblock as the request's description. - Path parameters:
:idin the URL becomes{{id}}, with the value fromparams:path. auth:becomes@auth: bearer, basic, API key in a header or the query, and OAuth 2 client credentials and password grants. A mode with no equivalent is written out by name so you can see what to replace.- The
assertblock becomes> assertlines.eq,neq, the four comparisons,contains,notContains,matches,startsWith,endsWith,length,isDefined,isEmptyand the null and truthiness checks all have one. vars:post-responsebecomes> set name = body…, andvars:pre-requestbecomes variables on the request.- Environments under
environments/become the values of anenvironmentdimension inenvironment.stamp. Names fromvars:secretcome across without values, because Bruno keeps secret values out of its files, and a note says to fill them inenvironment.local.stamp.
What does not, which it tells you about rather than dropping quietly: script:pre-request is not converted. Common lines of script:post-response and tests are, and the rest are kept as comments above the request. Scripts attached to the collection or to a folder are reported by name. A .bru request that is not HTTP is named and skipped.
Variable names that are not identifiers are renamed the same way everywhere, api-key to apiKey, and an import never takes a name the workspace already declares, so it cannot change what an existing request sends. Run stamp check on the result before anything else: it parses the folder and sends nothing.
Side by side
| Stampdrill | Bruno | |
|---|---|---|
| Requests on disk | .stamp text, several requests per file | .bru files, one request each |
| In git | the whole folder | the whole folder |
| Account to run it | none | none |
| Logic past a check | expressions, lambdas and fn in the same file | JavaScript in script: and tests blocks |
| Environments | a dimension per axis, combined | one file per environment |
| Platforms | macOS for the app, macOS and Linux for stamp | macOS, Windows and Linux |
| Open source | engine and command line tool (MPL 2.0); the Mac app is not | yes |
| Price | $49.99 once for the app, command line free | the client is free |
If you need Windows, or a client whose own source you can change, Bruno is the answer and this is a short comparison. On a Mac, the question is what you want the file to be able to say: if it keeps turning out to be the plan, the load test and the race belonging next to the request that they exercise, in one language and one review, that is what this is. Trying it costs an import and a stamp check, because the command line tool is free: start at the quick start, or read Bringing collections in first.
The guide Quick start Stampdrill and Postman
Updated