Next Best Action Development: How to Boost ROAS 4–8x

Classic rule-based segments no longer work: customers are tired of spam, and budgets melt away on ineffective touchpoints. We build an AI-powered Next Best Action system that leverages Reinforcement Learning to decide what to offer each customer at any given moment—from a discount to a manager's call. We deliver turnkey, from audit to implementation and ongoing support, making your marketing personalized and efficient.

AI Development Areas

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1344
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1306
  • B2B Advance company logo design
    B2B Advance company logo design
    753
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1049
  • AIDER company logo development
    AIDER company logo development
    992
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1097

Next Best Action Development: How to Boost ROAS 4–8x

How Next Best Action Transforms Marketing

You launch an email campaign, look at the dashboard — conversion drops, churn grows. Classic rule-based segments no longer work: customers are tired of spam, and the budget is melting away on ineffective touchpoints. We build an AI-powered Next Best Action (NBA) system that decides what to do with each customer right now — send a discount code, connect a manager, or do nothing. We rely on Reinforcement Learning and real-time business constraints.

In A/B testing, we consistently see LTV increase by 20–40% and churn decrease by 20–30%. On one project, savings reached $14k–20k per month by cutting ineffective communications. The ML action model is trained on historical data to personalize communications and optimize marketing. The system delivers real-time recommendations for each customer.

Problems NBA Solves

Excessive communication. Without NBA, you bombard everyone with identical emails. A customer who just bought receives a discount offer — irritation, churn. We implement a penalty for frequency: after 2 touches in 7 days, the action probability decreases by a factor of 0.4.

Wrong channel. Push notifications are cheap, but for B2B clients they are useless — a manager call is needed. For mass-market, a 50-dollar call is unprofitable. NBA ranks actions by expected ROI, considering channel cost.

Static rules. Rules like "if LTV > 1000 — call" become outdated in a month. NBA retrains on fresh data — reward functions are tied to actual revenue.

How We Build NBA: Contextual Bandit with RL

The core is a contextual bandit. For each action, we train a separate LogisticRegression model that predicts the probability of high reward (LTV delta) given the context. Multiple actions may be candidates; we select the one maximizing expected ROI.

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler
import pandas as pd

class NextBestActionEngine:
    """Contextual bandit for selecting the next action"""
    def __init__(self):
        self.actions = {
            'email_discount': {'cost': 10, 'type': 'outbound'},
            'push_notification': {'cost': 1, 'type': 'outbound'},
            'call_outbound': {'cost': 50, 'type': 'outbound'},
            'show_banner': {'cost': 0.5, 'type': 'inbound'},
            'offer_upgrade': {'cost': 0, 'type': 'inbound'},
            'no_action': {'cost': 0, 'type': 'none'}
        }
        self.action_models = {}  # Separate model for each action
        self.scaler = StandardScaler()

    def train(self, history: pd.DataFrame):
        """
        history: user_id, context_features..., action_taken, reward (LTV delta)
        """
        X = self.scaler.fit_transform(
            history.drop(columns=['user_id', 'action_taken', 'reward']).fillna(0)
        )
        for action in self.actions:
            mask = history['action_taken'] == action
            if mask.sum() < 50:
                continue
            # Model: under which context does the action yield high reward
            action_rewards = history.loc[mask, 'reward']
            y = (action_rewards > action_rewards.median()).astype(int)
            model = LogisticRegression(C=1.0, max_iter=200)
            model.fit(X[mask], y)
            self.action_models[action] = model

    def recommend_action(self, user: dict, business_constraints: dict = None) -> dict:
        """Select optimal action"""
        context_features = self._extract_context(user)
        X = self.scaler.transform([context_features])
        action_scores = {}
        for action, meta in self.actions.items():
            # Apply business constraints
            if business_constraints:
                if meta['cost'] > business_constraints.get('max_action_cost', 1000):
                    continue
                if (action in business_constraints.get('blocked_actions', [])):
                    continue
            # Predict reward
            if action in self.action_models:
                reward_prob = self.action_models[action].predict_proba(X)[0][1]
            else:
                reward_prob = 0.3  # Prior for untrained actions
            # Expected ROI = expected revenue - cost
            expected_revenue = reward_prob * user.get('expected_clv', 100)
            expected_roi = expected_revenue - meta['cost']
            # Fatigue from communications
            if meta['type'] == 'outbound':
                communications_7d = user.get('communications_7d', 0)
                fatigue_penalty = max(0, 1 - 0.3 * communications_7d)
                expected_roi *= fatigue_penalty
            action_scores[action] = {
                'expected_roi': expected_roi,
                'reward_probability': reward_prob,
                'cost': meta['cost']
            }
        # Select action with maximum ROI
        best_action = max(action_scores, key=lambda x: action_scores[x]['expected_roi'])
        return {
            'recommended_action': best_action,
            'expected_roi': action_scores[best_action]['expected_roi'],
            'reward_probability': action_scores[best_action]['reward_probability'],
            'all_scores': action_scores
        }

    def _extract_context(self, user: dict) -> list:
        return [
            user.get('days_since_last_purchase', 30),
            user.get('total_orders', 0),
            user.get('ltv', 0),
            user.get('churn_probability', 0.5),
            user.get('email_open_rate', 0.2),
            user.get('age_months', 12),
            user.get('avg_order_value', 100),
            user.get('support_tickets_30d', 0),
            user.get('website_visits_7d', 0),
            user.get('communications_7d', 0)
        ]

How to Orchestrate NBA in Real Time?

NBA is effective only when it reacts instantly. A customer abandons a cart — within 5 minutes they receive a push with a personalized offer. We build an event-driven pipeline: Kafka captures events (view, cart, ticket), enriches the profile, calls recommend_action, and sends a command to the executor.

class NBAOrchestrator:
    """Real-time orchestration of actions"""

    def __init__(self, nba_engine: NextBestActionEngine):
        self.engine = nba_engine
        self.action_executors = {}

    def process_customer_event(self, event_type: str, user: dict) -> dict:
        """Event-triggered processing"""
        # Event context affects NBA
        event_modifiers = {
            'cart_abandoned': {'churn_probability': +0.2, 'urgency': 'high'},
            'product_viewed_3x': {'intent_score': 0.8},
            'price_page_viewed': {'price_sensitivity': +0.3},
            'support_ticket_opened': {'satisfaction': -0.5}
        }

        enriched_user = {**user}
        if event_type in event_modifiers:
            for key, delta in event_modifiers[event_type].items():
                if isinstance(delta, (int, float)):
                    enriched_user[key] = enriched_user.get(key, 0) + delta
                else:
                    enriched_user[key] = delta

        # Constraints based on event
        constraints = {}
        if event_type == 'cart_abandoned':
            constraints['allowed_actions'] = ['email_discount', 'push_notification', 'show_banner']

        decision = self.engine.recommend_action(enriched_user, constraints)

        # Log for training
        self._log_decision(user['user_id'], event_type, decision)
        return decision

    def _log_decision(self, user_id: str, event: str, decision: dict):
        """Record decision for offline training"""
        import json
        import datetime
        log_entry = {
            'timestamp': datetime.datetime.now().isoformat(),
            'user_id': user_id,
            'trigger_event': event,
            'action_taken': decision['recommended_action'],
            'expected_roi': decision['expected_roi']
        }
        # In production: write to Kafka/DB
        print(f"NBA: {json.dumps(log_entry)}")

Why NBA with RL Is Better Than Rules?

Parameter Rule-based approach Our NBA (Contextual Bandit)
Adaptability Weekly review Real-time, online learning
Context consideration 2–3 features 10+ features, event modifiers
Optimal "do nothing" No Yes (up to 40% of customers)
ROAS 2–3x 4–8x
Churn reduction 10–15% 20–30%

The difference is 3x in ROAS. And the key insight: for 30–40% of customers, the best action is nothing. That's not an error; it's budget savings and churn reduction.

How NBA Boosts ROAS 4–8x

The system learns from historical data, identifying patterns: under which context which action yields maximum ROI. The reward function penalizes excessive communications and rewards actions leading to purchase. A/B tests consistently show a stable increase in ROAS compared to rules.

Typical Mistakes When Implementing NBA

  • Ignoring cost per action — leads to choosing expensive channels without budget oversight.
  • Absence of a "do nothing" signal — the model will always spam, increasing churn.
  • Insufficient history for training — <100k records yields high variance.

Implementation Timeline

Stage Duration Key result
Analytics and data collection 1–2 weeks Defined action list, cleaned data
Model design 1 week Selected features, reward function, constraints
Development and training 2–4 weeks Working bandit on 100k+ records
Integration and testing 2 weeks A/B test launched
Deployment and monitoring 1 week Production with p99 latency <100ms

Basic MVP with 5–6 actions and a contextual bandit: 4–8 weeks. Full system with RL and event orchestration: 10–16 weeks. Cost is calculated individually based on your data volume and number of channels.

What's Included (Deliverables)

  • Audit of current marketing data and channels.
  • Design of action model (up to 10 actions).
  • Training of contextual bandit on historical data.
  • Development of event orchestrator (Kafka/PubSub).
  • Integration with CRM, ESP, push services.
  • A/B testing vs rule-based segments.
  • Documentation and team training.
  • Quality guarantee: one-month trial run with metric monitoring.

Our team has years of experience developing AI solutions for marketing. Our engineers are authors of open-source RL libraries. We provide a guarantee on model correctness for 3 months after launch.

Get a consultation right now. Contact us for an audit of your data. Order end-to-end NBA development and see first results in 4 weeks.