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
X-Tallyfy-Client header and wrong grant type or expired tokens and this guide covers how to diagnose and fix every common error including 401 authentication problems and 404 path mistakes and 422 validation failures and rate limiting and file upload issues along with ready-to-use debugging scripts for your Postman collection. Postman > Working with templates and processes
Was this helpful?
- 2025 Tallyfy, Inc.
- Privacy Policy
- Terms of Use
- Report Issue
- Trademarks