The engine ships as collection data and runs where your requests run — two passes over one shared ctx, both driven by a single override block at the top of a request's Tests tab.
override or in the suite-wide hephaestus.defaults.
Assertions post-request
The assertions map is the workhorse: a field path maps to an object of operators, and every operator on that field must pass. Operators combine, so one entry can check existence, type and range at once. Two modifiers ride alongside them — soft logs a warning instead of failing, and when skips the check unless its expression is truthy. Setting softFail: true globally makes every functional failure soft.
assertions: { "data.id": { exists: true, type: "number" }, "data.email": { matches: "@" }, "data.score": { gte: 0, lte: 100 }, "data.name": { minLen: 1, maxLen: 100 }, "data.role": { includes: "admin" }, "status": { eq: "active", when: "ctx.response.code === 200", soft: true }, "error": { absent: true }, }
keysToFind, varsToSave, keysToCount
Three lighter helpers sit next to the map. keysToFind asserts that fields exist at the given paths — a bare string, or a { path, expect } object to also match a value (it honours soft and when too). varsToSave pulls a value out of the response and stores it in a Postman variable, keyed by alias, with scope of collection (default), environment or local. keysToCount asserts how many entries sit at a path — an array by default, or an object when you set type: "object".
keysToFind: [ "data.id", { path: "data.token", expect: "abc" }, ], varsToSave: { token: { path: "data.token", scope: "collection" }, }, keysToCount: { items: { path: "data.items", expected: 3 }, }
Shape, order & headers post-request
Above the per-field map sit five declarative checks for structure and collections. Each one is a single object and emits its own Postman test.
assertShape · assertOrder · assertUnique
assertShape is a one-liner per field: a path maps to an expected type — "string", "number", "boolean", "object", "array", plus "absent" (must not exist) and "any" (must exist, any type). assertOrder confirms an array is sorted by a field in a direction, comparing as "number" or "string". assertUnique confirms elements are unique — by whole value, or by a nested field with by.
assertShape: { "data": "object", "data.id": "number", "data.items": "array", "error": "absent", // must not exist "meta": "any", // any type, present }, assertOrder: { path: "data.items", by: "price", direction: "asc", type: "number", }, assertUnique: { path: "data.items", by: "id" }
assertEach — every element against the same rules
Run the assertion vocabulary over each item of an array. Give it a path, optional minCount / maxCount for length, and a rules object using the same operators as the map above. Any rule can be marked soft to warn per item instead of failing.
assertEach: { path: "data.products", minCount: 1, maxCount: 200, rules: { id: { type: "number", gt: 0 }, name: { type: "string", minLen: 1 }, price: { gte: 0 }, active: { eq: true, soft: true }, }, }
assertHeaders — one test per header
An array of entries, each keyed by name. Unless it is an absence check, every entry first asserts the header exists and is a non-empty string, then applies a matcher: expect as a substring or a predicate, equals for an exact value, or absent: true. equals wins when both are given; a throwing predicate counts as a failed check, not a broken run. The predicate form is a function, so it only works in a JS override, not in JSON defaults. Add label to name the test.
assertHeaders: [ { name: "X-Request-Id" }, { name: "Content-Type", expect: "json" }, { name: "X-Api-Version", equals: "v2" }, { name: "X-Powered-By", absent: true }, { name: "X-RateLimit-Remaining", label: "rate limit left", expect: v => Number(v) > 0 }, ]
Snapshots post-request
Snapshot regression pins a response against a saved baseline. On the first run the engine stores the body in collectionVariables["hephaestus.snapshots"], keyed by collection::request::status::format; later runs compare against it. It stays off unless enabled: true (or record: true). ignorePaths drops volatile fields before comparing; checkPaths narrows the comparison to just those sub-paths.
- strict — full deep-equal, minus
ignorePaths. - non-strict — the default: every key in the baseline must be present now, but new keys are allowed.
- structural — compares shape only (leaf path → type). A field added, removed or retyped fails; volatile ids, timestamps and counters never register as drift.
When an endpoint changes on purpose, set snapshotRecord: true (shorthand for snapshot.record), run once to overwrite the baseline, then drop the flag. Storage defaults to "collection-vars", the only backend that works offline; "postman-api" is unavailable offline, so the engine warns once and falls back automatically.
snapshot: { enabled: true, mode: "structural", // shape only ignorePaths: ["data.updatedAt", "meta.ts"], checkPaths: ["data.status", "data.type"], }, // force a fresh baseline for one run, then remove: snapshotRecord: true
Schema, GraphQL & retries post-request
schema validates the parsed response against a full JSON Schema through a bundled tv4 (draft-04 / draft-07) — no network, no extra dependency. It is off unless enabled: true.
graphql checks the contract a 200 hides: GraphQL answers 200 even when the body carries an errors[] array, so status alone proves nothing. Passing true is shorthand for { noErrors: true }; you can also assert errorCount / errorContains for negative tests and shape the payload under data with dataShape.
retryOnStatus re-sends the request while its status is in the list, using setNextRequest — and it short-circuits the rest of the pipeline on intermediate attempts, so a retry never runs half a set of assertions. Turn on respectRetryAfter to obey the server's Retry-After header, waiting at most retryAfterCapMs so a hostile value can't stall the run.
schema: { enabled: true, schema: { type: "object", required: ["id"] }, }, graphql: { noErrors: true }, // 200 + errors[] fails retryOnStatus: { statuses: [503, 429], maxRetries: 3, respectRetryAfter: true, // obey Retry-After retryAfterCapMs: 5000, }
Security & output post-request
securityAudit runs passive checks on the response and emits a separate test for each finding, so failures surface individually. It is opt-in — the default is { enabled: false, checkCors: true }. Enabled with no lists, it applies OWASP-style defaults: it requires HSTS, CSP, X-Frame-Options and X-Content-Type-Options; forbids server, x-powered-by and x-aspnet-version; and scans the body for stack traces and database errors. Version 4 added cookieFlags, checkJwt (rejects alg:none and expired tokens) and requireNoStore. Providing your own list replaces the matching built-in default.
Localization. locale selects the language of every user-facing string the engine emits — generated test names, logs, errors and status labels. "ru" is the default and is byte-identical to prior releases; "en" is the same engine in English. Assertion behaviour never changes, only the text. Any locale the catalog carries is now selectable, and npm run check:locales validates a contribution for completeness and parity.
Payload budget and config safety. maxBytes caps response size in bytes (values <= 0 disable it) — the size counterpart to maxResponseTime. strictMode turns the typo-guard strict: an unknown top-level override key normally warns with a "did you mean" suggestion, but under strictMode: true it fails the run so CI blocks on the typo. extraKeys is the escape hatch — list the names a third-party plugin reads off ctx.config and both the warning and the strict failure go away for exactly those keys.
securityAudit: { enabled: true, checkJwt: true, // alg:none / expired cookieFlags: true, // Secure, HttpOnly, SameSite requireNoStore: true, }, maxBytes: 1048576, // 1 MB response budget locale: "en", // "ru" default, or "en" strictMode: true, // unknown keys fail the run extraKeys: ["budgetMs"], // allowlist plugin keys
The CLI
Outside Postman lives a single zero-dependency binary. It never touches the engine — it reads what Newman wrote and turns each metric into a gate, exiting non-zero on the specific thing that regressed. Install it globally, then feed it a run report.
Plugins & the gallery post-request
Extensibility follows the engine-as-data model: a plugin is a single file you paste into a collection variable and register in the collection's Pre-request script — no build step, no engine fork. Plugins run once per request, second to last in the pipeline: after securityAudit, before the log summary. That position is what makes them useful — every assertion verdict is already in ctx._meta.results and ctx.config is still live. A throw inside a plugin becomes a failing test named after it, and the rest still run; if retryOnStatus fires, the pipeline stops before plugins and they sit out that iteration.
The gallery under gallery/plugins/ ships four working plugins plus a commented skeleton:
response-budget.js— fails a request that misses its latency budget, with per-request budgets, soft mode and a warning band.timing-histogram.js— collects response times and prints an ASCII histogram with p50/p90/p95/max and the slowest three.pii-redactor.js— finds credential- and PII-shaped values, reports them masked, and registers their field names so the summary redacts them too.csv-metrics.js— appends one CSV row per request and prints the table between cut markers at the end of a run._template.js— a commented skeleton documenting everyctxsurface a plugin can touch.
Gallery keys are read off ctx.config, so they live in your config where the engine has never heard of them — none are in KNOWN_KEYS. Each therefore warns by default and fails under strictMode, so declare them with extraKeys.
// 1. paste the file into a collection variable: // hephaestus.plugin.budget = <response-budget.js> // 2. register in the collection Pre-request script: pm.collectionVariables.set("hephaestus.plugins", JSON.stringify([ { name: "response-budget", post: "hephaestus.plugin.budget" }, ])); // 3. its keys live in ctx.config — declare them // when strictMode is on: const override = { extraKeys: ["budgetMs", "budgets"], budgetMs: 800, };