Provisioning 100 Voice Agents in 10 Minutes: A Batch Guide
You've signed 100 dental practices. Each one needs a WFW voice agent. You could provision them one at a time through the dashboard — or you could run a script that finishes before your coffee gets cold.
This post covers the complete batch provisioning pattern: reading a customer list, queuing requests to stay inside rate limits, polling for completion, handling failures gracefully, and validating the results. All examples use the /v2/ API paths.
The Pattern at a Glance
Read customers.csv
│
▼
Queue 100 POST /v2/agents requests
(5 concurrent max — rate limit headroom)
│
▼
Collect operation_ids
│
▼
Poll each operation until active or failed
(exponential backoff, 3-minute timeout per agent)
│
▼
Log failures, continue with the rest
│
▼
GET /v2/agents?status=active to validate final count
Straightforward. The details are in the concurrency control, the polling strategy, and the failure handling.
Concurrency: Don't Fire 100 Simultaneous Requests
The agents:write rate limit is tighter than read operations. Firing all 100 provisioning requests simultaneously will exhaust your budget, trigger 429 errors, and turn a 10-minute job into an hour-long retry loop.
The right approach is a queue with controlled concurrency — 5 simultaneous requests is safe for most service accounts:
// Process an array of tasks with a concurrency limit
async function pLimit(tasks, concurrency) {
const results = [];
const queue = [...tasks];
const active = new Set();
await new Promise((resolve) => {
function next() {
while (active.size < concurrency && queue.length > 0) {
const task = queue.shift();
const promise = task().then(result => {
results.push(result);
active.delete(promise);
if (queue.length === 0 && active.size === 0) resolve();
else next();
}).catch(err => {
results.push({ error: err.message });
active.delete(promise);
if (queue.length === 0 && active.size === 0) resolve();
else next();
});
active.add(promise);
}
}
next();
});
return results;
}
Five concurrent requests means 100 agents queue in roughly 20 requests × ~6 seconds each = 2 minutes of API calls, leaving 8 minutes for Workforce Wave provisioning to complete in parallel.
The Full Provisioning Script
const fs = require('fs');
const fetch = require('node-fetch');
const API_BASE = 'https://api.workforcewave.com';
const TOKEN = process.env.WFW_API_TOKEN;
const CONCURRENCY = 5;
// Read customers from CSV: customer_id,business_name,business_url
function readCustomers(csvPath) {
return fs.readFileSync(csvPath, 'utf8')
.split('\n')
.slice(1) // skip header
.filter(line => line.trim())
.map(line => {
const [customer_id, business_name, business_url] = line.split(',');
return { customer_id: customer_id.trim(), business_name: business_name.trim(), business_url: business_url.trim() };
});
}
// POST /v2/agents — uses customer_id as idempotency key so reruns are safe
async function provisionAgent(customer) {
const res = await fetch(`${API_BASE}/v2/agents`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
// Idempotency key derived from customer_id — rerunning the script won't create duplicates
'Idempotency-Key': `provision-customer-${customer.customer_id}`,
},
body: JSON.stringify({
payload: {
name: `${customer.business_name} AI Receptionist`,
business_url: customer.business_url,
template_id: 'dental_receptionist',
}
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`Provision failed for ${customer.customer_id}: ${err.error?.message}`);
}
const data = await res.json();
return { customer_id: customer.customer_id, operation_id: data.data.operation_id };
}
// Poll GET /v2/operations/{id} until active or failed — give up after 3 minutes
async function pollOperation(operationId, customerId) {
const deadline = Date.now() + 3 * 60 * 1000; // 3 minutes
let delay = 2000; // start at 2s, increase exponentially
while (Date.now() < deadline) {
await new Promise(r => setTimeout(r, delay));
delay = Math.min(delay * 1.5, 15000); // cap at 15s
const res = await fetch(`${API_BASE}/v2/operations/${operationId}`, {
headers: { 'Authorization': `Bearer ${TOKEN}` },
});
const data = await res.json();
if (data.data.status === 'active') {
return { customer_id: customerId, status: 'active', agent_id: data.data.agent_id };
}
if (data.data.status === 'failed') {
return { customer_id: customerId, status: 'failed', error: data.data.error?.message };
}
// status === 'pending' — keep polling
}
return { customer_id: customerId, status: 'timeout', error: 'Provisioning exceeded 3 minutes' };
}
async function main() {
const customers = readCustomers('customers.csv');
console.log(`Provisioning ${customers.length} agents...`);
// Step 1: Fire provisioning requests with concurrency limit
const provisionTasks = customers.map(c => () => provisionAgent(c).catch(err => ({
customer_id: c.customer_id, error: err.message
})));
const provisionResults = await pLimit(provisionTasks, CONCURRENCY);
// Separate successes from immediate failures (bad URL, auth error, etc.)
const succeeded = provisionResults.filter(r => r.operation_id);
const failed = provisionResults.filter(r => r.error);
console.log(`Provisioning queued: ${succeeded.length} succeeded, ${failed.length} failed immediately`);
if (failed.length > 0) console.error('Immediate failures:', failed);
// Step 2: Poll all operation_ids in parallel (polling is cheap — read rate limit)
const pollTasks = succeeded.map(r => () => pollOperation(r.operation_id, r.customer_id));
const pollResults = await pLimit(pollTasks, 20); // polling can be more concurrent
// Step 3: Report results
const active = pollResults.filter(r => r.status === 'active');
const pollFailed = pollResults.filter(r => r.status !== 'active');
console.log(`\nProvisioning complete:`);
console.log(` Active agents: ${active.length}`);
console.log(` Failures: ${pollFailed.length}`);
if (pollFailed.length > 0) {
console.error('Failed agents:', pollFailed);
fs.writeFileSync('failed-agents.json', JSON.stringify(pollFailed, null, 2));
console.log('Failures written to failed-agents.json for manual review.');
}
}
main().catch(console.error);
Idempotency for Reruns
The idempotency key provision-customer-${customer.customerid} is the most important line in the script. If the script crashes midway — network drop, process kill, provisioning timeout — you can rerun it against the same CSV. WFW recognizes the keys it's already seen and returns the original operationid without creating new agents for customers that were already provisioned.
Without this, a crash at customer 47 means customers 1–46 get a second agent provisioned on the next run. With it, rerunning is safe. Always derive idempotency keys from durable business identifiers, not from UUIDs generated at runtime.
Error Handling: Log and Continue
Some agents will fail. A customer might have a broken website that Workforce Wave can't crawl. A DNS timeout. A malformed URL in the CSV. The right response is to log the failure and continue with the rest — not to abort the entire batch.
The script above writes failures to failed-agents.json for manual review after the batch completes. Common failure modes:
scoutcrawlfailed— Workforce Wave couldn't access the business URL. Check the URL, try again manually.invalid_url— the URL was malformed. Fix the CSV.templatenotfound— thetemplate_iddoesn't exist for your account. Check available templates in the dashboard.
For a production batch job, consider sending the failure list to a monitoring channel (Slack, PagerDuty) rather than just writing a JSON file.
Post-Batch Validation
After the batch completes, verify the count:
curl "https://api.workforcewave.com/v2/agents?status=active&limit=200" \
-H "Authorization: Bearer $WFW_API_TOKEN" \
| jq '.meta.total'
The meta.total field gives the total count of active agents matching the query, regardless of the page size. If you provisioned 100 and expected 98 to succeed (based on your failure log), and meta.total shows 98, the batch is complete and accurate.
This post completes the SaaS Integration series. For the rate limit and idempotency patterns referenced here, see the previous post: Rate Limiting and Idempotency: What Your Bot Needs to Know.
Ready to put AI voice agents to work in your business?
Get a Live Demo — It's FreeContinue Reading
Related Articles
Workforce Wave AI: The Engine Behind Auto-Provisioning
What happens inside the 5-step Workforce Wave pipeline when a partner enters a business URL, why partners get an operationId instead of a 30-second wait, and how ww_operations powers the fleet dashboard progress bar.
Rate Limiting and Idempotency: What Your Bot Needs to Know
The two most important API patterns for AI consumers of the WFW API — with concrete examples and a production-ready TypeScript client.
The Bot Creation Matrix: Four Ways to Deploy AI, Now All Live on WFW
Dual-mode agent support just shipped, completing the Bot Creation Matrix. WFW is now the only platform where a bot can be the creator and the consumer — entirely human-free.