Call Flow Script: Call Intercept and Rerouting

IliasL_3CX

Staff member
Joined
Jun 2, 2014
Messages
147
Reaction score
118

Route inbound calls dynamically based on Caller ID, DID and time of day.​

This script allows you to redirect inbound calls based on specific criteria such as time of day, Caller ID or DID. It is activated when a call is received on a trunk and reroutes the call to a configured destination.

How to Set Up the Time-Based Call Routing Script

...
Continue reading the Original Blog Post.
 
With this new system of Call Scripts, will the CFD scripts stop working at some point? I know a lot of people use them.
 
CFD outputs call scripts, so they will not stop working...
 
  • Is it possible to configure a quantity of DIDs, each with a different destination?
  • Is it possible to configure defined a destination out of this working hours?
 
Yes its possible but you have to study the script. And if its not possible you can adjust it to do whatever you want.
 
Does anyone have an example of a script that routes a batch of DIDs to different destinations?
 
Does anyone have an example of a script that routes a batch of DIDs to different destinations?
This is available in the script store, directly from within the PBX

1738682479671.png

1738682494733.png

1738682522658.png

It has all the information you need in there

1738682562594.png
 
I made a script modifying the time-based routing script.
It looks up part of the DID, then it verifies if the corresponding sales department is on break, if so it sends the call to the queue of the local reception, if not it sends the call to an IVR (which asks the customer if he wants to join sales).
We have two locations with reception and sales department.
The script works fine, however it always throws a warning - should I just ignore it or what should be corrected in the code?
2025/04/16 15:20:19.377|0011|Warn| [_3CX.CallPair] _pausestarv_234.InterceptInboundCall.13002.[C:2743.3]-Failed to process ICall.Terminate - connection does not exist
C#:
#nullable disable
using CallFlow;
using System;
using System.Threading.Tasks;
using TCX.Configuration;
using TCX.PBXAPI;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Linq;

namespace interceptcall
{
    public class InterceptInboundCall : ScriptBase<InterceptInboundCall>
    {
        // Dictionary / Array with part of DID (to identify called location), department to verify (check if on breaktime), DN (if on break), DN (default)
        readonly Dictionary<string, string[]> didDict = new Dictionary<string, string[]> {
                { "12345678", new[] { "test sales", "800", "100" } },
                { "1234", new[] { "thisdepartment sales", "890", "895" } },
                { "456", new[] { "other sales", "990", "995" } }
                };

        public override async void Start()
        {
            // Handle calls as a detached task, catching all exceptions.
            try
            {
                await Task.Run(async () =>
                {
                    try
                    {
                        MyCall.Debug($"Script start delay: {DateTime.UtcNow - MyCall.LastChangeStatus}");
                        MyCall.Debug($"Incoming connection {MyCall} from {MyCall.Caller}");
                        bool intercepted = false;
                        var ps = MyCall.PS as PhoneSystem;

                        var match = Regex.Match(MyCall.Caller["inbound_did"], @"^(?:\+49|\*49|0)?(\d+)$");
                        if (!match.Success) return;

                        var local = match.Groups[1].Value;
                        var DIDNumber = didDict.Keys.OrderByDescending(k => k.Length).FirstOrDefault(local.StartsWith);

                        if (DIDNumber != null && MyCall.Caller.DN is ExternalLine externalLine)
                        {
                            string[] didArray = didDict[DIDNumber];
                            var currentTime = externalLine.Now(out var utc, out var timezone, out var groupmode);
                            
                            TCX.Configuration.Group g = ps.GetGroupByName(didArray[0]);
                            var destination_struct = new DestinationStruct();

                            // Intercept calls during the specified schedule
                            if (CommonRoutingExtensions.IsBreak(g,currentTime))
                            {
                                destination_struct = new DestinationStruct(ps.GetDNByNumber(didArray[1]));
                                MyCall.Info($"{DIDNumber} {didArray[0]} 'isBreak' -> {didArray[1]}");
                            }
                            else
                            {
                                destination_struct = new DestinationStruct(ps.GetDNByNumber(didArray[2]));
                                MyCall.Info($"{DIDNumber} {didArray[0]} 'isNotBreak' -> {didArray[2]}");
                            }

                            try
                            {
                                var result = await MyCall.RouteToAsync(destination_struct);
                                MyCall.Debug($"{DIDNumber} has been redirected");
                                intercepted = true;
                            }
                            catch (Exception ex)
                            {
                                MyCall.Debug($"{DIDNumber}: interception failed. '' is not reachable: {ex}");
                            }
                        }

                        MyCall.Return(intercepted);
                    }
                    catch (Exception ex)
                    {
                        MyCall.Error($"Script execution failed: {ex}");
                        MyCall.Return(false);
                    }
                });
            }
            catch (Exception ex)
            {
                MyCall.Error($"Task execution failed: {ex}");
                MyCall.Return(false);
            }
        }
    }
}
 
  • Like
Reactions: fxbastler
A nice script.

Two comments:
  1. In line 40, the StartsWith - is that what you want?
  2. We always use the department number, not the name. The name can be changed at any time and it's annoying when various scripts stop working. The department number remains al long as the department exist.
We have something very similar with a few more functions in use: our incoming_calls.cs with different time-dependent forwarding options (own times from other department if desired) including forwarding to mailboxes. We integrated it into holiday.cs because there can only be one script that triggers on the trunk.

About your Problem: Failed to process ICall.Terminate - connection does not exist
I don't know how the script is called, but if possible and if it's enough we use only return with true or false because our start routine also expects a bool as a return value.
 
Last edited:
  • Like
Reactions: GMark
Thank you.

  1. StartsWith: I want the the best (most precise) match from the dictionary - therefore OrderByDescending and FirstOrDefault, FirstOrDefault in turn expects a "predicate" - StartsWith nicely fits what I need.
    But I am open for suggestions to shorten this line!
  2. You are right, I will modify the script and use the group numbers, and just precise in comment lines the according department names. If someone else has to change the script later...

We integrated it into holiday.cs because there can only be one script that triggers on the trunk.
There could be a second script as default route, I think.
Well, I do not need / want this script on the trunk, just on some DIDs. If I code them in the script this might become more confusing when changes need to be made. Maybe I am wrong - you have much more experience with 3CX.

Would you recommend to simply ignore the warning "InterceptInboundCall.13002.[C:2743.3]-Failed to process ICall.Terminate - connection does not exist" ?
 
  • Like
Reactions: N_G
There could be a second script as default route, I think.
Well, I do not need / want this script on the trunk, just on some DIDs. If I code them in the script this might become more confusing when changes need to be made. Maybe I am wrong - you have much more experience with 3CX.

... looks like I will integrate this in the holiday.cs
 
Last edited:
How can i except some DN form the timebasecallscript.cs (from the Store)?
Some help would be nice, THX!
 
Got it.

Ich hab es jetzt selber hinbekommen, falls es einer brauchen sollte:
/*
* Time Base Call Routing - Use code with caution!
* Calls will be intercepted and redirected to the specified DestinationDN during the following times:
* Monday to Sunday: 5:30 PM to 7:00 AM
* The current destination DN is set to "801" modify destination to any system extension or extension you want.
*
* INSTRUCTIONS
* - Configure Date & Time (Line 31)
* - Change the destination DN, and modify the value of the constant DestinationDN to any destination you want to route call (Line 26).
*/

/*
* Script Anpassung by OpIT GmbH
* Ausnahme Liste hinzugefügt
* static readonly string[] ExcludedDIDs = { "DID Nummer", "DID Nummer" };
* Dort die Externe und Interne Rufnummer angeben
*/

#nullable disable
using CallFlow;
using System;
using System.Threading;
using System.Threading.Tasks;
using TCX.Configuration;
using TCX.PBXAPI;
using System.Collections.Generic;
using System.Linq;
using CallFlow.CFD;

namespace interceptcall
{
public class InterceptInboundCall : ScriptBase<InterceptInboundCall>
{
// The destination DN to which the call will be redirected
const string DestinationDN = "496";

// List of DIDs that should not be intercepted (add your DIDs here)
static readonly string[] ExcludedDIDs = { "+4312345492", "492" };

// Define a schedule for when calls should be intercepted
static readonly Schedule schedule = new Schedule(RuleHoursType.SpecificHours)
{
{ DayOfWeek.Monday, new Schedule.PeriodOfDay(TimeSpan.FromHours(17.0), TimeSpan.FromHours(24)) },
{ DayOfWeek.Monday, new Schedule.PeriodOfDay(TimeSpan.FromHours(0), TimeSpan.FromHours(7.30)) },
{ DayOfWeek.Monday, new Schedule.PeriodOfDay(TimeSpan.FromHours(12), TimeSpan.FromHours(13)) },

{ DayOfWeek.Tuesday, new Schedule.PeriodOfDay(TimeSpan.FromHours(17.0), TimeSpan.FromHours(24)) },
{ DayOfWeek.Tuesday, new Schedule.PeriodOfDay(TimeSpan.FromHours(0), TimeSpan.FromHours(7.30)) },
{ DayOfWeek.Tuesday, new Schedule.PeriodOfDay(TimeSpan.FromHours(12), TimeSpan.FromHours(13)) },

{ DayOfWeek.Wednesday, new Schedule.PeriodOfDay(TimeSpan.FromHours(17.0), TimeSpan.FromHours(24)) },
{ DayOfWeek.Wednesday, new Schedule.PeriodOfDay(TimeSpan.FromHours(0), TimeSpan.FromHours(7.30)) },
{ DayOfWeek.Wednesday, new Schedule.PeriodOfDay(TimeSpan.FromHours(12), TimeSpan.FromHours(13)) },

{ DayOfWeek.Thursday, new Schedule.PeriodOfDay(TimeSpan.FromHours(17.0), TimeSpan.FromHours(24)) },
{ DayOfWeek.Thursday, new Schedule.PeriodOfDay(TimeSpan.FromHours(0), TimeSpan.FromHours(7.30)) },
{ DayOfWeek.Thursday, new Schedule.PeriodOfDay(TimeSpan.FromHours(12), TimeSpan.FromHours(13)) },

{ DayOfWeek.Friday, new Schedule.PeriodOfDay(TimeSpan.FromHours(12.0), TimeSpan.FromHours(24)) },
{ DayOfWeek.Friday, new Schedule.PeriodOfDay(TimeSpan.FromHours(0), TimeSpan.FromHours(8.0)) },

{ DayOfWeek.Saturday, new Schedule.PeriodOfDay(TimeSpan.FromHours(0), TimeSpan.FromHours(24)) },

{ DayOfWeek.Sunday, new Schedule.PeriodOfDay(TimeSpan.FromHours(0), TimeSpan.FromHours(24)) },
};

public override async void Start()
{
// Handle calls as a detached task, catching all exceptions.
try
{
await Task.Run(async () =>
{
try
{
MyCall.Debug($"Script start delay: {DateTime.UtcNow - MyCall.LastChangeStatus}");
MyCall.Debug($"Incoming connection {MyCall} from {MyCall.Caller}");
bool intercepted = false;
var ps = MyCall.PS as PhoneSystem;

if (MyCall.Caller.DN is ExternalLine externalLine)
{
var DIDNumber = MyCall.Caller["inbound_did"];
var CallerID = MyCall.Caller.CallerID;
var currentTime = externalLine.Now(out var utc, out var timezone, out var groupmode);

// Check if the DID should be excluded
bool isExcludedDID = ExcludedDIDs.Contains(DIDNumber);

if (schedule.IsActiveTime(currentTime) && !isExcludedDID)
{
string[] AllowedCallers = { "*" }; // Allow all Callers
var destination_struct = new DestinationStruct(ps.GetDNByNumber(DestinationDN));

// Check if the call's DID and CallerID match the interception criteria
if (AllowedCallers.Contains(CallerID) || AllowedCallers.Contains("*"))
{
try
{
var result = await MyCall.RouteToAsync(destination_struct);
MyCall.Info($"{CallerID} -> {DIDNumber} has been redirected to {DestinationDN} ({result})");
intercepted = true;
}
catch (Exception ex)
{
MyCall.Info($"{CallerID} -> {DIDNumber}: interception failed. '{DestinationDN}' is not reachable: {ex}");
}
}
else
{
MyCall.Info($"{CallerID} -> {DIDNumber}@{currentTime}: Default CallerID/DID based routing will be applied");
}
}
}

MyCall.Return(intercepted);
}
catch (Exception ex)
{
MyCall.Error($"Script execution failed: {ex}");
MyCall.Return(false);
}
});
}
catch (Exception ex)
{
MyCall.Error($"Task execution failed: {ex}");
MyCall.Return(false);
}
}
}
}
 
  • Like
Reactions: N_G

Members Online Now

Forum statistics

Threads
111,834
Messages
589,287
Members
164,662
Latest member
DejanMDS