Issue
In rare situations, TaskRouter Tasks can become stuck in a non-terminal state (pending, reserved, or assigned) after the underlying communication channel has already completed.
Normally, the Task state is driven by the associated channel (for example, when a Voice call ends, the Task automatically enters the wrapping state). During outages or abnormal conditions, TaskRouter and the channel can fall out of sync, requiring manual intervention.
Product
Flex
Environment
legacy Twilio Console
Cause
During outages or abnormal conditions, orchestration between TaskRouter and the underlying communication channel can become misaligned.
Resolution
This can be resolved by updating the stuck Task's assignmentStatus to a terminal state:
- If the Task was never assigned (
pendingorreserved): cancel it. - If the Task was assigned to an agent (
assigned): complete it.
How to determine if a voice Task is "stuck"
- First confirm that the Task in question is still active in TaskRouter by fetching the Task resource.
curl -X GET "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \
-u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKENIf you get a 404 and you know you have a valid Task SID, then the Task has likely already been closed for over 5 minutes. This means the Task is not stuck in TaskRouter. (If you are still seeing Task data in the Flex UI or elsewhere, this can indicate a different "stuck" scenario with the Flex UI.)
- Review the Task's
assignment_statusproperty to determine if its still open. - If the Task is still open, grab the Call SID from the Task's
attributes- look for thecall_sidproperty which is populated by Twilio during orchestration.
Task attributes example:
{"from_country":"US","called":"+1555xxxxxxx","call_sid":"CAb920ad5f119ab57be3fxxxxxxxx","from":"+1678xxxxxxx", "to":"+1555xxxxxxx","direction":"inbound"}- Once you have the Call SID, fetch the Call resource to see its state. Below is an example in CURL:
curl -X GET "https://api.twilio.com/2010-04-01/Accounts/ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/Calls/CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.json"
-u ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:your_auth_tokenThe following are terminal states for Calls: completed, failed, busy, no-answer
If the Call is no longer active, but the Task is still active, the Task can be considered "stuck".
NOTE: This guidance is intended for Flex environments with default voice orchestration. If your Flex instance has customized orchestration, please ensure this "stuck" Task logic won’t close legitimately active Tasks.
How to update Tasks
Tasks can be updated in the following ways:
- Individually from the Twilio Console > TaskRouter > Workspace > Tasks
- Individually or in bulk using Twilio's API or helper libraries
Solution Examples
Process Bulk Tasks with Node.js
For an example of how to approach a script using Twilio's Node.js helper library, see the following solution which:
- Fetches open Tasks (
pending,reserved,assigned) - Filters for only "voice" Tasks
- Finds the related call via
call_sidTask attribute - Fetches the Voice call to verify the call's status.
- If the call is in a terminal state (completed, failed, busy, or no-answer), it updates the Task accordingly:
-
assigned→completed -
pending/reserved→canceled
-
This approach helps clear stuck Tasks after outages while avoiding active ones.
Dependencies
Install the following npm packages:
twiliodotenv
Configure
Update with your environment-specific variables:
- Set your Twilio credentials (
TWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN) in your.envfile. - Set your workspace SID in the script.
Code
Save the following code as a file in your Node project:
// Twilio TaskRouter Stuck Task Cleaner
// Fetches open tasks, and closes those with completed calls
// Load environment variables
const dotenv = require('dotenv');
dotenv.config();
// Initialize Twilio Client with auto-retry
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = require('twilio')(accountSid, authToken,
{ autoRetry: true, maxRetries: 3 }
);
// Define workspace SID inline (replace with your actual workspace SID)
const workspaceSid = 'WSxxxxxxxxxxxxxxxxxxxx;
// Define custom reason for task closure for reporting
const voiceReason = 'Orchestration issue: closed stuck voice Task.';
// Define terminal call states
const TERMINAL_CALL_STATES = ["completed", "failed", "busy", "no-answer"];
// Main logic
(async () => {
let tasksUpdated = 0;
let processed = 0;
let skipped = 0;
try {
await new Promise((resolve, reject) => {
client.taskrouter.v1.workspaces(workspaceSid).tasks.each({
assignmentStatus: 'pending,reserved,assigned', // fetch only open Tasks
pageSize: 1000,
callback: async (task, done) => {
processed++;
let attributes = {};
try {
attributes = JSON.parse(task.attributes);
} catch {
console.log(`Could not parse attributes for task ${task.sid}`);
skipped++;
done();
return;
}
if (task.taskChannelUniqueName !== 'voice' || !attributes.call_sid) {
skipped++;
done();
return;
}
try {
const call = await client.calls(attributes.call_sid).fetch();
if (TERMINAL_CALL_STATES.includes(call.status)) {
const status = task.assignmentStatus === 'assigned' ? 'completed' : 'canceled';
await client.taskrouter.v1.workspaces(workspaceSid).tasks(task.sid).update({
assignmentStatus: status,
reason: voiceReason
});
console.log(`Updated voice task ${task.sid} to ${status}`);
tasksUpdated++;
}
} catch (err) {
console.log(`Error processing task ${task.sid}:`, err);
}
if (processed % 100 === 0) {
console.log(`Processed ${processed} tasks so far...`);
}
done();
},
done: error => {
if (error) reject(error);
else resolve();
}
});
});
console.log(`Updated ${tasksUpdated} tasks to completed/canceled.`);
console.log(`Processed ${processed} tasks, skipped ${skipped} tasks.`);
} catch (err) {
console.error('Error processing tasks:', err);
process.exit(1);
}
})();Run the script
Run node <YOUR_FILE_NAME>.js> to begin processing Tasks.