Back to Blog
FreeSWITCHESLNode.jsCall ControlPredictive DiallerReal-Time

FreeSWITCH ESL with Node.jsReal-Time Call Control

Build production ESL applications in Node.js — connect to FreeSWITCH Event Socket, subscribe to call events, implement DTMF routing, predictive diallers, and complex call flows with async event-driven architecture.

8021

Default ESL port

500/s

events Node.js handles

async

push-based events

bgapi

non-blocking commands

By Kaushik Parmar · Founder & VoIP Architect, CelloIP Technologies · January 29, 2026 · 16 min read

How do you control FreeSWITCH calls from Node.js using ESL?

Use the modesl npm package to connect to FreeSWITCH's Event Socket Layer (ESL) on port 8021. Subscribe to events like CHANNEL_ANSWER and CHANNEL_HANGUP, and issue commands using conn.bgapi() for non-blocking execution.

ESL: The Key to Unlocking FreeSWITCH

FreeSWITCH's dialplan XML handles simple routing efficiently. But for dynamic routing — database lookups, real-time agent assignment, custom conferencing, predictive dialling — you need the Event Socket Layer (ESL).

Direct Call Control

Execute FreeSWITCH API commands on live calls in real-time

Event Stream

Push-based events — subscribe to what you need, ignore the rest

Bi-Directional

Send commands while simultaneously receiving events from all channels

Inbound vs Outbound ESL

Inbound ESL

Your app connects to FreeSWITCH port 8021. Subscribe to events from ALL channels globally.

Predictive diallers
Agent state monitoring
CDR processing
Real-time dashboards

Outbound ESL

FreeSWITCH connects TO your app when a specific call enters the socket() dialplan application. Exclusive control of that call leg.

IVR applications
Per-call DTMF routing
Conference control
Dynamic hold music

Outbound ESL: DTMF-Driven Call Routing

FreeSWITCH connects to your Node.js server for each new inbound call. Your server answers, collects DTMF input, and routes based on digit pressed:

javascriptOutbound ESL — DTMF-driven call routing in Node.js
const esl = require('modesl');

// FreeSWITCH will connect here for each call (outbound ESL)
const server = new esl.Server({ port: 8084, myevents: true });

server.on('connection::ready', async (conn) => {
  const uuid = conn.getInfo().getHeader('Unique-ID');
  console.log(`New call: ${uuid}`);

  await conn.execute('answer');
  await conn.execute('playback', 'ivr/ivr-welcome_to_freeswitch.wav');
  await conn.execute(
    'play_and_get_digits',
    '1 1 3 5000 # ivr/ivr-enter_ext.wav ivr/ivr-invalid.wav DTMF_DIGIT \\d 3000'
  );

  conn.on('esl::event::CHANNEL_EXECUTE_COMPLETE::*', async (ev) => {
    if (ev.getHeader('Application') === 'play_and_get_digits') {
      const digit = ev.getHeader('Variable_DTMF_DIGIT');
      const dest = digit === '1' ? 'sales_queue'
                 : digit === '2' ? 'support_queue'
                 : 'default_queue';
      await conn.execute('transfer', dest);
    }
  });
});
Dialplan hook: Add <action application="socket" data="127.0.0.1:8084 async full"/> to your FreeSWITCH dialplan extension to connect each call to this server.

Inbound ESL: Predictive Dialler

For a predictive dialler, inbound ESL monitors agent availability across all channels. When an agent hangs up, immediately originate the next outbound call:

javascriptInbound ESL — agent state tracking + auto-originate
const esl = require('modesl');
const agentState = new Map(); // agentId → 'idle' | 'busy'

const conn = new esl.Connection('127.0.0.1', 8021, 'ClueCon');

conn.on('esl::ready', () => {
  conn.subscribe(['CHANNEL_ANSWER', 'CHANNEL_HANGUP', 'CUSTOM']);
  console.log('ESL connected, monitoring all channels');
});

conn.on('esl::event::CHANNEL_ANSWER::*', (ev) => {
  const agent = ev.getHeader('Variable_sip_to_user');
  if (agent) agentState.set(agent, 'busy');
});

conn.on('esl::event::CHANNEL_HANGUP::*', (ev) => {
  const agent = ev.getHeader('Variable_sip_to_user');
  if (agent) {
    agentState.set(agent, 'idle');
    originateNextCall(agent); // dial next lead immediately
  }
});

function originateNextCall(agentId) {
  const nextLead = getNextLead(); // your DB query
  if (!nextLead) return;
  const cmd = `originate sofia/gateway/trunk/${nextLead.number}`
    + ` &bridge(user/${agentId})`;
  conn.bgapi(cmd); // non-blocking — response via BACKGROUND_JOB event
}

// Auto-reconnect on FreeSWITCH restart
conn.on('error', () => {
  setTimeout(() => conn.connect(), 5000);
});

Key ESL Event Types

CHANNEL_CREATE

Track new calls entering the system

CHANNEL_ANSWER

Mark call answered, start billing

CHANNEL_HANGUP

End billing, update agent state

DTMF

Capture key presses in IVR

CHANNEL_EXECUTE_COMPLETE

Know when play_and_get_digits finishes

CUSTOM sofia::register

Track SIP endpoint registrations

BACKGROUND_JOB

Receive async bgapi() responses

CHANNEL_BRIDGE

Track when two call legs are connected

Production: PM2 + Auto-Reconnect

Production Checklist

Change 'ClueCon' password in event_socket.conf.xml
Bind ESL to 127.0.0.1, not 0.0.0.0
Implement reconnect loop for FS restarts
Use bgapi() — never api() in async handlers
Filter events — don't subscribe to ALL
PM2 cluster mode for multi-core
Redis for call state — survives restarts
jsonecosystem.config.js — PM2 cluster config
module.exports = {
  apps: [{
    name: 'esl-app',
    script: './src/index.js',
    instances: 'max',
    exec_mode: 'cluster',
    max_restarts: 50,
    restart_delay: 2000,
    env: {
      NODE_ENV: 'production',
      FS_HOST: '127.0.0.1',
      FS_PORT: 8021,
      FS_PASS: 'your_secure_password'
    }
  }]
}

Frequently Asked Questions

QWhat is the ESL port by default?

8021. Configure in /etc/freeswitch/autoload_configs/event_socket.conf.xml. Change the default 'ClueCon' password immediately in production.

QCan multiple applications connect to ESL simultaneously?

Yes. Multiple inbound ESL clients can connect to the same FreeSWITCH event socket. Each receives the same event stream independently.

QIs there a Python ESL library?

Yes — python-ESL (C binding) and pure-Python alternatives. Node.js modesl is the most actively maintained as of 2026 with async/await support.

QHow do I handle FreeSWITCH restarts in production?

Use the esl::reconnect event or implement a reconnect loop in your Node.js app. PM2's auto-restart handles process crashes. Store call state in Redis so it survives reconnects.

Build Your FreeSWITCH Platform

CelloIP Technologies builds production FreeSWITCH platforms — from ESL-powered call centres and predictive diallers to carrier-grade WebRTC switches. Expert configuration and custom development.