Postman > Collection organization best practices
Advanced patterns and testing
Collection runners handle bulk operations. Newman plugs into CI/CD pipelines. Monitors run scheduled checks. Together, these patterns take you from manual API testing to automated workflow validation.
If you work across several Tallyfy organizations, you’ll want a fast way to switch between them during testing.
Create a pre-request script that rotates through your orgs automatically:
// Pre-request script to rotate organizationsconst orgs = [ { name: "Production", id: "org_prod_123", clientId: "client_prod", clientSecret: pm.environment.get("PROD_SECRET") }, { name: "Staging", id: "org_stage_456", clientId: "client_stage", clientSecret: pm.environment.get("STAGE_SECRET") }];
let currentIndex = pm.variables.get("ORG_INDEX") || 0;
const currentOrg = orgs[currentIndex];pm.environment.set("TALLYFY_ORG_ID", currentOrg.id);pm.environment.set("TALLYFY_CLIENT_ID", currentOrg.clientId);pm.environment.set("TALLYFY_CLIENT_SECRET", currentOrg.clientSecret);
console.log(`Testing with ${currentOrg.name} organization`);
const nextIndex = (currentIndex + 1) % orgs.length;pm.variables.set("ORG_INDEX", nextIndex);Compare processes across organizations after fetching from each:
const orgProcesses = pm.environment.get("ORG_PROCESSES") || {};const currentOrg = pm.environment.get("TALLYFY_ORG_ID");
// Tallyfy wraps responses in a "data" propertyorgProcesses[currentOrg] = pm.response.json().data;pm.environment.set("ORG_PROCESSES", orgProcesses);
const orgIds = Object.keys(orgProcesses);if (orgIds.length >= 2) { console.log("Process count comparison:"); orgIds.forEach(orgId => { console.log(`${orgId}: ${orgProcesses[orgId].length} active processes`); });
// Find processes with the same name across orgs const processNames = new Set(); orgIds.forEach(orgId => { orgProcesses[orgId].forEach(p => processNames.add(p.name)); });
processNames.forEach(name => { const orgsWithProcess = orgIds.filter(orgId => orgProcesses[orgId].some(p => p.name === name) ); if (orgsWithProcess.length > 1) { console.log(`"${name}" exists in ${orgsWithProcess.length} orgs`); } });}Add this to your collection’s Tests tab to track response times over multiple runs:
pm.test("Response time is acceptable", function () { pm.expect(pm.response.responseTime).to.be.below(1000);});
const perfData = pm.environment.get("PERFORMANCE_DATA") || [];perfData.push({ endpoint: pm.request.url.toString(), method: pm.request.method, responseTime: pm.response.responseTime, timestamp: new Date().toISOString(), status: pm.response.code});
// Keep last 100 entriesif (perfData.length > 100) perfData.shift();pm.environment.set("PERFORMANCE_DATA", perfData);
const recentTimes = perfData.slice(-10).map(d => d.responseTime);const avgTime = recentTimes.reduce((a, b) => a + b, 0) / recentTimes.length;
if (avgTime > 800) { console.warn(`Performance degradation detected. Avg: ${avgTime}ms`);}const perfData = pm.environment.get("PERFORMANCE_DATA") || [];const endpointStats = {};
perfData.forEach(entry => { // Normalize UUIDs and numeric IDs to /:id const endpoint = entry.endpoint.replace(/\/[a-f0-9\-]{8,}/g, '/:id');
if (!endpointStats[endpoint]) { endpointStats[endpoint] = { count: 0, totalTime: 0, maxTime: 0, minTime: Infinity }; }
const stats = endpointStats[endpoint]; stats.count++; stats.totalTime += entry.responseTime; stats.maxTime = Math.max(stats.maxTime, entry.responseTime); stats.minTime = Math.min(stats.minTime, entry.responseTime);});
Object.entries(endpointStats).forEach(([endpoint, stats]) => { const avg = (stats.totalTime / stats.count).toFixed(0); console.log(`${endpoint}: ${stats.count} calls, avg ${avg}ms, min ${stats.minTime}ms, max ${stats.maxTime}ms`);});Postman mock servers let you simulate API responses without hitting the real Tallyfy API. They match requests by HTTP method, path, query parameters, and headers like x-mock-response-code or x-mock-response-name.
// Save successful responses as mock examplesif (pm.response.code >= 200 && pm.response.code < 400) { const examples = pm.environment.get("MOCK_EXAMPLES") || {}; const key = `${pm.request.method}_${pm.request.url.getPath().replace(/\//g, '_')}`;
examples[key] = { request: { method: pm.request.method, url: pm.request.url.toString(), headers: pm.request.headers.toObject(), body: pm.request.body ? pm.request.body.raw : null }, response: { status: pm.response.code, headers: pm.response.headers.toObject(), body: pm.response.text() }, timestamp: new Date().toISOString() };
pm.environment.set("MOCK_EXAMPLES", examples);}const mockConfig = { development: { useMock: true, mockUrl: "https://mock-server-123.pstmn.io" }, staging: { useMock: false, realUrl: "https://go.tallyfy.com/api" }, production: { useMock: false, realUrl: "https://go.tallyfy.com/api" }};
const env = pm.environment.get("TARGET_ENV") || "development";const config = mockConfig[env];
if (config.useMock) { pm.request.url.host = config.mockUrl.replace(/https?:\/\//, '').split('/'); pm.request.url.protocol = "https";
const scenario = pm.environment.get("MOCK_SCENARIO") || "success"; pm.request.headers.add({ key: 'x-mock-response-name', value: scenario });}const errorSimulation = { "rate_limit": { headers: {"x-mock-response-code": "429"} }, "server_error": { headers: {"x-mock-response-code": "500"} }, "timeout": { headers: {"x-mock-response-code": "408"} }};
const simulateError = pm.environment.get("SIMULATE_ERROR");if (simulateError && errorSimulation[simulateError]) { Object.entries(errorSimulation[simulateError].headers).forEach(([key, value]) => { pm.request.headers.add({key, value}); });}| Feature | Newman | Postman CLI |
|---|---|---|
| Installation | npm install -g newman | Download from Postman |
| Authentication | API key only | Full OAuth support |
| Cloud features | Limited | Full workspace sync |
| CI/CD maturity | Well-established | Newer, growing |
| Extensibility | Rich plugin system | Limited but improving |
# Install Newman (requires Node.js v16+)npm install -g newmannewman --version# Basic run with reportingnewman run tallyfy-api.postman_collection.json \ -e production.postman_environment.json \ --reporters cli,json,html \ --reporter-json-export results.json \ --reporter-html-export report.html \ --delay-request 100 \ --timeout-request 30000
# Data-driven runnewman run collection.json \ -e environment.json \ -d test-data.csv \ --iteration-count 5
# Stop on first failurenewman run collection.json \ --bail failure \ --global-var "API_BASE=https://go.tallyfy.com/api"
# Run a specific folder onlynewman run collection.json \ --folder "Authentication Tests" \ --env-var "SKIP_CLEANUP=true"Here’s a working pipeline that tests against multiple environments:
.github/workflows/api-tests.yml:
name: Tallyfy API Tests
on: schedule: - cron: '0 */4 * * *' workflow_dispatch: push: branches: [main, develop] pull_request: branches: [main]
env: NODE_VERSION: '18'
jobs: api-tests: runs-on: ubuntu-latest strategy: matrix: environment: [staging, production] test-suite: [smoke, full]
steps: - uses: actions/checkout@v4
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'npm'
- name: Install Newman run: npm install -g newman newman-reporter-htmlextra
- name: Run API tests env: TALLYFY_CLIENT_ID: ${{ secrets[format('TALLYFY_CLIENT_ID_{0}', matrix.environment)] }} TALLYFY_CLIENT_SECRET: ${{ secrets[format('TALLYFY_CLIENT_SECRET_{0}', matrix.environment)] }} run: | newman run postman/tallyfy-api.json \ -e postman/${{ matrix.environment }}.json \ --folder "${{ matrix.test-suite }}" \ --env-var "TALLYFY_CLIENT_ID=$TALLYFY_CLIENT_ID" \ --env-var "TALLYFY_CLIENT_SECRET=$TALLYFY_CLIENT_SECRET" \ --reporters cli,json \ --reporter-json-export results-${{ matrix.environment }}-${{ matrix.test-suite }}.json \ --delay-request 100 \ --timeout-request 30000 \ --bail failure continue-on-error: true
- name: Parse results id: test-results run: | RESULT_FILE="results-${{ matrix.environment }}-${{ matrix.test-suite }}.json" if [ -f "$RESULT_FILE" ]; then TOTAL=$(jq '.run.stats.requests.total' "$RESULT_FILE") FAILED=$(jq '.run.stats.requests.failed' "$RESULT_FILE") echo "total_requests=$TOTAL" >> $GITHUB_OUTPUT echo "failed_requests=$FAILED" >> $GITHUB_OUTPUT fi
- name: Upload artifacts uses: actions/upload-artifact@v4 if: always() with: name: test-results-${{ matrix.environment }}-${{ matrix.test-suite }} path: results-*.json retention-days: 30
- name: Fail on test failures if: steps.test-results.outputs.failed_requests > 0 run: | echo "API tests failed: ${{ steps.test-results.outputs.failed_requests }}/${{ steps.test-results.outputs.total_requests }}" exit 1newman run collection.json \ --reporters cli,json \ --reporter-json-export current-results.json
# Compare against a saved baselinenode scripts/performance-comparison.js \ --baseline baseline-results.json \ --current current-results.json \ --threshold 20Postman supports CSV and JSON data files. CSV works for flat data; JSON handles nested structures.
CSV example - test-data.csv:
process_name,template_id,assignee,expected_status"Q1 Budget Review","template_123","john@company.com","active""Employee Onboarding","template_456","hr@company.com","pending"JSON example - test-data.json:
[ { "process_name": "Q1 Budget Review", "template_id": "template_123", "assignee": "john@company.com", "kick_off_data": { "field_department": "Finance", "field_budget_amount": 50000 }, "expected_tasks": 5, "validation_rules": { "response_time_max": 2000, "required_fields": ["id", "name", "status"] } }]Using data variables in tests:
const expectedTasks = parseInt(pm.variables.get("expected_tasks"));const validationRules = JSON.parse(pm.variables.get("validation_rules") || '{}');
pm.test(`Process has ${expectedTasks} tasks`, () => { const response = pm.response.json(); pm.expect(response.data.tasks).to.have.lengthOf(expectedTasks);});
if (validationRules.response_time_max) { pm.test(`Response under ${validationRules.response_time_max}ms`, () => { pm.expect(pm.response.responseTime).to.be.below(validationRules.response_time_max); });}
if (validationRules.required_fields) { validationRules.required_fields.forEach(field => { pm.test(`Has field: ${field}`, () => { pm.expect(pm.response.json().data).to.have.property(field); }); });}Error scenario data file:
[ { "scenario": "invalid_template_id", "template_id": "invalid_123", "expected_status": 404 }, { "scenario": "missing_required_field", "template_id": "template_123", "kick_off_data": {}, "expected_status": 422 }]const scenario = pm.variables.get("scenario");const expectedStatus = parseInt(pm.variables.get("expected_status"));
pm.test(`${scenario} returns ${expectedStatus}`, () => { pm.expect(pm.response.code).to.equal(expectedStatus);});Structure your collection to mirror a full workflow, passing data between requests:
// Collection order:// 1. Authenticate// 2. Create process (POST /organizations/{org}/runs)// 3. Complete tasks (PUT /organizations/{org}/runs/{run}/tasks/{task})// 4. Add comment// 5. Verify process complete
// In "Create Process" Tests tab - note the .data wrapper:const processId = pm.response.json().data.id;pm.collectionVariables.set("CURRENT_PROCESS_ID", processId);
// Subsequent requests reference {{CURRENT_PROCESS_ID}}Fire multiple requests at once to compare response times:
async function parallelOperations() { const operations = [ { name: "List Templates", endpoint: "/checklists" }, { name: "List Processes", endpoint: "/runs" }, { name: "List Tasks", endpoint: "/me/tasks" }, { name: "List Users", endpoint: "/users" } ];
const results = await Promise.all( operations.map(op => pm.sendRequest({ url: `${pm.environment.get("TALLYFY_BASE_URL")}/organizations/${pm.environment.get("TALLYFY_ORG_ID")}${op.endpoint}`, method: 'GET', header: { 'Authorization': `Bearer ${pm.environment.get("TALLYFY_ACCESS_TOKEN")}`, 'X-Tallyfy-Client': 'APIClient' } }).then(response => ({ name: op.name, status: response.code, count: response.json().data?.length || 0, time: response.responseTime })) ) );
results.forEach(r => { console.log(`${r.name}: ${r.count} items in ${r.time}ms`); });}
parallelOperations();Postman monitors run collections on a schedule. Two things worth monitoring:
Stuck process detection:
pm.test("No stuck processes", function() { const processes = pm.response.json().data; const stuckCount = processes.filter(p => { const hoursSinceUpdate = (Date.now() - new Date(p.updated_at)) / 3600000; return hoursSinceUpdate > 24 && p.status === 'active'; }).length;
pm.expect(stuckCount).to.equal(0);});API availability:
pm.test("API is responsive", function() { pm.response.to.have.status(200); pm.expect(pm.response.responseTime).to.be.below(2000);});if (pm.test.failures && pm.test.failures.length > 0) { pm.sendRequest({ url: pm.environment.get("SLACK_WEBHOOK_URL"), method: 'POST', header: { 'Content-Type': 'application/json' }, body: { mode: 'raw', raw: JSON.stringify({ text: "Tallyfy API Monitor Alert", attachments: [{ color: "danger", fields: [ { title: "Failed Tests", value: pm.test.failures.map(f => f.name).join("\n") }, { title: "Environment", value: pm.environment.name, short: true }, { title: "Time", value: new Date().toISOString(), short: true } ] }] }) } });}// Slow down when the API responds slowlyconst lastResponseTime = pm.environment.get("LAST_RESPONSE_TIME");if (lastResponseTime > 2000) { pm.environment.set("REQUEST_DELAY", 500);} else { pm.environment.set("REQUEST_DELAY", 100);}["TEMP_PROCESS_ID", "TEMP_TASK_DATA", "CACHED_RESPONSE", "ITERATION_STATE"] .forEach(key => pm.environment.unset(key));Api Clients > Getting started with Postman API testing
Postman > Troubleshooting common issues
Postman > Working with templates and processes
Was this helpful?
- 2025 Tallyfy, Inc.
- Privacy Policy
- Terms of Use
- Report Issue
- Trademarks