Chapter 2.6 Follow-up and Timing Systems

Sections 5.1 through 5.5 built a CRM automation system capable of receiving a lead, structuring and validating it, enriching it with a hybrid scoring assessment, routing it to the correct lifecycle stage, and notifying the appropriate team members with a complete audit record. Every Contact that enters the brokerage’s HubSpot instance through Workflow A arrives correctly classified, scored, prioritized, and assigned a follow-up Task. The system’s intake behavior is, by the end of Chapter 2.5, architecturally complete.

What Chapter 2.5 does not address is what happens after the Task is created. The qualification Switch assigns a priority label and routes the notification. The task creation node assigns a due date. The Slack notification reaches the broker. And then the automation stops. The broker either acts on the notification or does not. The Task either gets completed or it doesn’t. The Contact either receives a callback within the SLA window or they don’t. The system has no memory of what should happen next, no mechanism for enforcing the SLA, and no path for escalating a lead that has been ignored.

This gap is not a corner case. In sales automation, the period between initial classification and first meaningful human engagement is the most failure-prone interval in the entire revenue pipeline. A lead that scored Hot and was routed to a broker with a 2-hour callback SLA but received no callback for 48 hours has not just missed a deadline it has likely been lost.

Commercial real estate leads have finite windows of active search. A contact who submits an inquiry and receives no response within 24–48 hours will almost certainly reach out to a competitor. The intake automation that correctly classified and routed the lead delivered no value if the follow-up layer does not enforce the downstream engagement.

Chapter 2.6 introduces timing as a first-class architectural concern. The core thesis is that successful automation is not only about making the correct decision, but also about making it at the correct time and about recovering gracefully from the inevitable cases where that time passes without the expected action. The section introduces three categories of timing and follow-up capability: strategy design (defining what the follow-up cadence should be for each segment), delayed execution control (implementing the timing mechanisms that schedule and trigger deferred actions), and retry and escalation logic (handling the non-response and failure cases that occur in production). Together, these capabilities extend the brokerage’s automation from an intake classification system into a contact-engagement system: one that actively monitors for engagement gaps and takes structured action to close them.

The practical extension introduced in Chapter 2.6 adds a third workflow to the evolving architecture Workflow C: the Follow-up Monitor that runs on a configurable schedule, queries HubSpot for overdue Tasks, and executes a priority-aware retry and escalation sequence. Workflow A and Workflow B from Sections 5.1–5.5 are extended with timing metadata writes that provide Workflow C with the information it needs to apply the correct follow-up behavior for each Contact.

Learning Objectives

After completing this chapter, you will be able to:

  • Design and build Workflow C: a time-triggered SLA enforcement workflow that monitors assigned tasks, sends tiered reminder notifications, and escalates to the operations manager when the SLA window closes without action.
  • Implement the three-tier follow-up escalation ladder (first reminder, second reminder, operations manager escalation) with configurable delay intervals for each priority tier.
  • Explain the distinction between notification delivery and follow-up enforcement, and describe the operational failure mode that arises when a system delivers notifications but does not enforce SLAs.
  • Configure the HubSpot task completion check in Workflow C so that escalation fires only for contacts whose assigned task remains incomplete after the SLA window, not for contacts where the broker has already taken action.
  • Troubleshoot a follow-up system where escalation notifications are firing for contacts the broker has already called, by identifying the task status check logic that is producing false positives.

2.6.1 Follow-up Strategy Design

Business Scenario

The brokerage’s intake automation (Workflows A and B) reliably classifies, scores, and notifies on every new lead. Brokers receive a Slack alert and a Task assignment within seconds of form submission. Despite this, the Hot lead SLA compliance rate measured in production is 34% meaning that 66% of Hot leads do not receive a first callback within the 2-hour SLA window. The system created the Task. The broker received the notification. Nothing that follows is guaranteed.

The Problem

The automation has no memory of what should happen after initial notification. A Task either gets completed or it does not. A Contact either receives a callback within the SLA window or does not. Without a follow-up enforcement layer, accurate classification at intake delivers no guarantee of engagement within the conversion window.

The Architectural Solution

A scheduled monitor workflow (Workflow C) is introduced alongside an external timing store pattern: follow-up due timestamps are pre-computed at intake and written to HubSpot Contact properties. Workflow C polls every 30 minutes, compares current time against stored timestamps, and executes a priority-aware reminder and escalation sequence for every Contact whose Task remains incomplete past its SLA threshold.

Updated Workflow

Workflow C’s polling and branching structure and how it connects to Workflow A’s timing metadata is shown in Figure 23.1.

%%{init:{"theme":"base","themeVariables":{"primaryColor":"#eef2ff","primaryTextColor":"#1e1b4b","primaryBorderColor":"#6366f1","lineColor":"#6366f1","clusterBkg":"#f8f9ff","clusterBorder":"#6366f1","titleColor":"#1e1b4b","edgeLabelBackground":"#f6f4ef","fontFamily":"system-ui,sans-serif","fontSize":"13px"}}}%%

flowchart TD
    subgraph WA["Workflow A Intake and Timestamp Write"]
        A["Lead Intake (Workflow A)"]:::trigger --> B["Contact Classified + Task Created"]:::success
        B --> C["Follow-up Timing Metadata Written to HubSpot"]:::success
    end

    subgraph WC["Workflow C Follow-Up Monitor"]
        D["Workflow C Polls Every 30 Minutes"]:::process --> E{"Evaluate Contact State vs. Due Timestamps"}:::decision
        E -->|"Task Complete"| F(["Skip Task Complete"]):::process
        E -->|"TP1 Overdue"| G["Send Reminder"]:::process
        E -->|"TP2 Overdue"| H["Send Second Reminder + Ops Alert"]:::process
        E -->|"Escalation Overdue"| I["Create Ops Task + Escalation Note"]:::success
    end

    C --> D
    classDef trigger  fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:600
    classDef process  fill:#eef2ff,stroke:#6366f1,color:#1e1b4b
    classDef decision fill:#fef9c3,stroke:#ca8a04,color:#78350f,font-weight:600
    classDef success  fill:#d1fae5,stroke:#059669,color:#064e3b,font-weight:700
    classDef fallback fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
Figure 23.1: Workflow C Follow-Up Enforcement. Workflow C follow-up enforcement: Workflow A writes due timestamps; the monitor polls every 30 minutes, skipping completed tasks and escalating through three tiers.

Follow-up Strategy

A follow-up strategy is the structured definition of what communication or engagement actions should occur after an initial contact event, in what order, at what intervals, through what channels, and under what conditions. It is the business specification from which the timing automation is built. Follow-up strategies are defined at the segment level different priority tiers, lifecycle stages, and contact characteristics call for different cadences and they precede the implementation of any timing mechanism.

The follow-up strategy has five components. The trigger is the event that initiates the sequence in the brokerage’s system, the trigger is the completion of Workflow A’s intake processing and the creation of the initial follow-up Task. The cadence is the schedule of touchpoints: how many, how far apart, and what the time limit is after which the sequence is considered concluded without response. The channel is the medium through which each touchpoint occurs phone, email, Slack notification, or some combination. The content specification defines what should be communicated at each touchpoint and how it should differ from previous touchpoints (escalating urgency, different value proposition, etc.). The escalation condition is the threshold typically a number of missed touchpoints or an elapsed time without response that triggers a handoff from the standard follow-up sequence to a different process.

The follow-up strategy is a business specification the automation is its technical implementation. Keeping these two layers separate ensures the business can revise the strategy without requiring an engineer to modify the workflow.

The distinction between a follow-up strategy and a follow-up automation is architectural. The strategy is a business specification: it can be written on a whiteboard, reviewed by sales leadership, and adjusted based on conversion data without any workflow changes. The automation is the technical implementation of the strategy: it executes the cadence, selects the channel, constructs the content, and activates the escalation path. Keeping these two layers separate the strategy in a configuration object, the automation in workflow nodes ensures that the business can revise the strategy without requiring an engineer to modify the workflow.

Engineers who build follow-up automation without a defined strategy first build systems that optimize around the implementation rather than the business objective. A common result is a system whose cadence is determined by what was technically convenient to implement rather than what the data suggests is optimal. A 24-hour follow-up interval is commonly implemented because it maps naturally to a daily schedule trigger not because conversion data demonstrates that 24 hours is the optimal first follow-up interval for a specific lead segment.

The follow-up strategy also determines the boundary conditions that the automation must handle: what happens if the first touchpoint is never attempted (broker absence), what happens if the contact responds before the second touchpoint is due (sequence should stop), what happens if the sequence completes without any response (contact should be reclassified or archived). These boundary conditions cannot be defined after the automation is built; they must be in the strategy specification, because the automation can only handle cases that were anticipated in its design.

For the automation engineer, the follow-up strategy specification is also a scope document. When the sales team asks for a new variation “can we add an extra touch for referrals?” or “should we use email instead of Slack for the third touchpoint?” the engineer can evaluate whether the requested change requires a strategy update (configuration change) or a structural automation change (new workflow node or branch). Most strategy updates should be configuration changes; structural automation changes should be rare.

The brokerage’s follow-up strategy is defined at the intersection of two dimensions: priority label (Hot, Warm, Cool, Cold) and follow-up event type (initial reminder, second reminder, escalation). The strategy is documented in a configuration object that governs Workflow C’s behavior.

Hot leads (combined_score ≥ 20): - Touchpoint 1: Broker reminder via #sales-alerts-hot Slack channel. Trigger: 2 hours after Task creation if Task not marked complete. Content: “OVERDUE [Contact Name] at [Company] has not been contacted. Hot lead SLA exceeded. Assigned to [Broker Name].” - Touchpoint 2: Second reminder + ops escalation. Trigger: 6 hours after Task creation if still not complete. Content: same as Touchpoint 1 with added “OPS ALERT: This Hot lead has been uncontacted for 6 hours. Escalating to [Operations Manager].” - Escalation: 24 hours without Task completion → create a new high-priority Task assigned to the operations manager; set escalation_triggered = true on Contact; log escalation event in structured Note.

Warm leads (combined_score 13–19): - Touchpoint 1: 24 hours after Task creation. Reminder to #sales-alerts channel. - Touchpoint 2: 72 hours. Second reminder with escalation to ops. - Escalation: 7 days without Task completion → create escalation Task; set escalation_triggered = true.

Cool leads (combined_score 7–12): - Touchpoint 1: 7 days after Task creation. Reminder to #crm-ops-intake. - Touchpoint 2: 14 days. Final reminder with ops notification. - Escalation: 21 days → escalation Task; escalation_triggered = true.

Cold leads (combined_score < 7): - No automated reminders. Single escalation at 30 days without Task completion: ops notification only, no new Task.

This strategy is stored as a configuration object in n8n under the environment variable namespace FOLLOWUP_CONFIG_* one variable per priority tier, storing the JSON object for that tier’s cadence. Workflow C reads these variables at execution time, making the strategy revision a configuration change rather than a workflow edit.

The follow-up strategy has a direct dependency on three outputs from the Chapter 2.5 intake workflow. The priority_label determines which strategy cadence applies. The Task creation timestamp stored as a HubSpot custom property followup_task_created_at written by Workflow A is the anchor from which all follow-up timing intervals are calculated. The assigned_owner_id is the broker to whom reminders are addressed and directed.

Key Principle

The follow-up strategy must be defined before any timing automation is built. The automation can only handle cases that were anticipated in the strategy specification boundary conditions, stop conditions, and escalation paths must all exist in the strategy before they can exist in the workflow.

The strategy also has a dependency on the Contact’s Task completion status. Workflow C must be able to query HubSpot for Tasks associated with a given Contact and determine whether the initial follow-up Task has been marked complete. This query is the entry point for the follow-up monitor’s per-Contact evaluation: if the Task is complete, no reminder is needed; if it is overdue by the cadence specification, the appropriate reminder or escalation is triggered.

CautionProduction Risk

The most common follow-up strategy design mistake is building the timing mechanism before defining the strategy it should execute. Engineers who implement a “send a reminder after 24 hours” node without specifying what the reminder should say, to whom it should be addressed, under what conditions it should fire, and when it should stop are building automation that is structurally correct but strategically undefined. The strategy must precede the implementation not because the strategy is theoretically prior, but because the implementation cannot handle cases that were not anticipated in the strategy specification.

CautionProduction Risk

A follow-up cadence that is identical across all priority tiers treats a top-scoring referral and a bottom-scoring cold inquiry as equivalent follow-up obligations which is operationally incorrect and wastes broker capacity on low-probability contacts. Hot leads and Cold leads have different conversion probabilities, different urgency profiles, and different broker capacity requirements.

CautionProduction Risk

The follow-up strategy must be calibrated against the expected lead volume and team capacity. A cadence that sends 3 reminders over 7 days for every Warm lead, a 2-reminder sequence for every Cool lead, and escalation tasks for all three tiers may produce a volume of automated notifications and tasks that exceeds what the sales and operations teams can process without becoming desensitized to the alerts.


2.6.2 Time-Based Triggers

Wall-Clock Triggers

Wall-clock triggers fire at an absolute calendar time at 9am every Monday, at the end of each business day, on the first of each month.

Relative Triggers

Relative triggers fire a specified interval after a preceding event 2 hours after Task creation, 24 hours after Contact creation, 7 days after lifecycle stage change.

Time-based triggers are the mechanisms that cause automation workflows to execute at a specific time rather than in response to an immediate event. They fall into these two categories. Both categories are essential in CRM automation; their appropriate use depends on whether the timing is anchored to a fixed schedule or to a per-contact event.

In n8n, time-based triggers are implemented through three primary mechanisms. The Schedule Trigger node fires a workflow at a configurable cron expression, making it the standard mechanism for wall-clock scheduling: daily digests, weekly reports, periodic consistency checks, and scheduled monitors that evaluate a population of records. The Wait node suspends execution of a running workflow for a specified duration before proceeding to the next node making it the mechanism for relative scheduling within a single workflow execution. Event-based polling a workflow triggered by a webhook or schedule that queries an external system for records meeting a time-based condition is the third pattern, used when the relative timing must be tracked outside the n8n execution context (for example, when delays are longer than the n8n execution timeout or when the timing metadata must be stored in HubSpot rather than in n8n’s execution state).

The selection of the right trigger mechanism has significant operational consequences. A Wait node that holds an execution open for 24 hours consumes an active execution slot in n8n for the full duration, which constrains parallelism and increases resource utilization. A scheduled monitor that runs every 30 minutes and queries HubSpot for overdue records uses execution slots only during its brief active windows and can handle an unbounded number of in-flight follow-up sequences without proportionally increasing resource consumption.

The trade-off is query complexity. The scheduled monitor must re-derive the relevant time context from HubSpot on every polling cycle, whereas the Wait node holds this context in the execution state without an external query. The decision between these two patterns is a resource-vs-complexity trade-off that every timing implementation must resolve.

The distinction between wall-clock and relative triggers has architectural implications that surface in production. Wall-clock triggers are easy to reason about “this runs at 9am” is unambiguous but they cannot adapt to per-contact timing without reading that timing from an external store. Relative triggers adapt naturally to per-contact timing but require a mechanism for persisting the timing context across the delay, which may be the n8n execution state (for short delays) or an external system like HubSpot (for delays longer than the execution context should hold).

Time-based triggers also interact with business hours in ways that require explicit handling. A relative trigger of “24 hours after Contact creation” that fires at 3am on a Sunday produces a Slack notification that no one will read until Monday morning at which point the notification is 30+ hours old and has scrolled out of view. A business-hours-aware follow-up system computes the next follow-up time as the next occurrence of a business-hours time point that is at least the configured interval after the anchor event. This computation is more complex than a simple interval addition, but it produces notifications that are actionable at the moment they are received.

For the automation engineer, time-based trigger design requires asking three questions for every timed action: What is the anchor event for this action? How long after the anchor event should this action fire? And does the firing time need to be aligned with business hours? The answers determine which trigger mechanism is appropriate and what pre-computation is needed.

The brokerage’s follow-up monitoring system uses a hybrid approach that combines all three trigger mechanisms for different purposes.

Workflow C (Follow-up Monitor) uses a Schedule Trigger: every 30 minutes during business hours (8am–8pm), 7 days a week. The cron expression is 0,30 8-20 * * *. This is wall-clock scheduling: Workflow C runs on a fixed cadence regardless of how many contacts are in follow-up. Each execution queries HubSpot for Tasks that match overdue criteria, evaluates the timing for each, and takes action for the ones that are past their follow-up threshold. The 30-minute polling interval means that a 2-hour Hot lead SLA breach will be detected within 30 minutes of occurring.

Workflow A (Intake) writes two HubSpot custom properties after each intake event: followup_task_created_at (the ISO 8601 timestamp of Task creation, used by Workflow C as the anchor for relative interval calculations) and followup_schedule_tier (a copy of priority_label at intake time, stored separately so that a priority label change does not retroactively alter the follow-up schedule). These two fields give Workflow C everything it needs to compute whether a Contact’s follow-up is overdue without requiring a separate timing state store.

Email delivery tracking (introduced in the Practical Implementation) uses a relative trigger: an HTTP Request node calls the email delivery provider’s status API with a 48-hour check window to confirm that a follow-up email was delivered and not bounced. This is event-based polling within a fixed time window rather than a persistent schedule.

Business hours awareness in n8n requires a utility function that computes the next business-hours timestamp from an arbitrary anchor time. The brokerage’s system implements this as a JavaScript Code node with the following logic: given a target timestamp (anchor + configured interval), if the result falls outside business hours (before 8am or after 8pm), advance to the next business-hours window (8am the next calendar day). If the result falls on a weekend, advance to 8am Monday. The function also accepts a US federal holiday list (stored as an n8n environment variable as a JSON array of date strings) to skip federal holidays in the calculation.

This utility function is called by Workflow A when computing the initial followup_task_due_at field written to the Contact record, and by Workflow C when computing overdue status. Both workflows use the same Code node logic, which is implemented as a reusable function stored in n8n’s Code node library (or duplicated with a version comment in both workflows, since n8n does not have a native shared function library).

All timestamps in the system are stored in UTC. The business hours window (8am–8pm) is defined in the brokerage’s local time zone (America/New_York), stored as the n8n environment variable BROKERAGE_TIMEZONE. The business hours utility function converts the anchor timestamp from UTC to the brokerage’s local time zone before applying the window check, then converts the result back to UTC for storage.


Diagram 2.6.1 Follow-up Timing Architecture

Anchor event: followup_task_created_at (written by Workflow A). Poll cycle: Workflow C runs every 30 minutes (8am–8pm, 7 days). Timezone: BROKERAGE_TIMEZONE (America/New_York).

Table 23.1
Priority Touchpoint 1 Touchpoint 2 Escalation
Hot (score ≥ 20) +2 biz hrs #sales-hot broker alert +6 biz hrs #sales-hot + ops alert +24 biz hrs Ops Task + escalation_triggered flag
Warm (score 13–19) +24 biz hrs #sales-alerts broker alert +72 biz hrs + ops alert +7 cal days Ops Task + escalation_triggered flag
Cool (score 7–12) +7 cal days #crm-ops broker alert +14 cal days + ops alert +21 cal days Ops Task + escalation_triggered flag
Cold (score < 7) none none +30 cal days ops notify only, no new Task

Condition for all touchpoints: Task associated with Contact is not marked “completed,” escalation_triggered property is false, and the Task is not deleted.

Stop conditions:

  • Task marked completed → clear all pending follow-up timers
  • Contact lifecycle stage changes to “customer” → stop sequence
  • escalation_triggered = true → stop standard touchpoints
  • contact_status = "disqualified" (lifecyclestage = “other”) → stop

Configuration storage: n8n environment variables, one per priority tier, each storing a JSON object:

FOLLOWUP_CONFIG_HOT   = {"tp1_hrs":2,  "tp2_hrs":6,  "esc_hrs":24, ...}
FOLLOWUP_CONFIG_WARM  = {"tp1_hrs":24, "tp2_hrs":72, "esc_days":7, ...}
FOLLOWUP_CONFIG_COOL  = {"tp1_days":7, "tp2_days":14,"esc_days":21,...}
FOLLOWUP_CONFIG_COLD  = {"esc_days":30,...}

CautionProduction Risk

Hard-coding timing values as constants within workflow node expressions rather than reading them from configuration variables creates a maintenance liability. An n8n Code node that contains const FIRST_FOLLOWUP_HOURS = 2 is less maintainable than one that reads const FIRST_FOLLOWUP_HOURS = parseInt(process.env.FOLLOWUP_HOT_TP1_HOURS). When the operations director decides that the Hot lead SLA should be tightened from 2 hours to 90 minutes, the configuration variable approach requires updating one environment variable; the hard-coded approach requires finding every location in the workflow where 2 was used for this purpose an error-prone search.

NoteEngineering Rationale

Polling interval and SLA duration must be designed together. A Workflow C that polls every 30 minutes can detect a SLA breach at most 30 minutes after it occurs. If the Hot lead SLA is 2 hours and Workflow C polls every 30 minutes, the actual guaranteed detection time is up to 2 hours and 30 minutes after Task creation. This 25% variance is acceptable for a 2-hour SLA. For a 30-minute SLA, the same polling interval would mean the breach might not be detected until it is 100% overdue which defeats the purpose of the SLA.

CautionProduction Risk

Business hours awareness is not a nice-to-have; it is required for any timing system that is supposed to produce actionable notifications. A Contact classified as Hot on a Friday at 4pm whose first follow-up fires “2 business hours” later should generate a broker alert on Monday at 10am not on Friday at 6pm (after business hours) or on Sunday at midnight (meaningless).


2.6.3 Delayed Execution Control

Delayed execution is the ability to schedule an action to occur at a future time within the context of an automation workflow, without requiring the automation engineer to create a separate scheduled workflow for every possible delay scenario. It is the difference between “run this action now” and “run this action in N minutes/hours/days from now, then continue the workflow from this point.” The engineering challenge of delayed execution is that the executing system n8n is an active process that consumes resources. An execution that is delayed for 24 hours holds an execution slot open for 24 hours. An execution that is delayed for 30 days is untenable in any system with finite concurrency limits.

The practical consequence is that delayed execution strategies must be selected based on delay duration relative to the system’s execution context constraints.

Short Delays (seconds to minutes)

Short delays can be implemented as Wait nodes within a single workflow execution without meaningful resource impact.

Medium Delays (hours to a few days)

Medium delays require a deliberate choice. Wait nodes are technically functional but consume long-lived execution slots. An external timing store (HubSpot properties, a database, or a scheduled monitor) is more resource-efficient but architecturally more complex.

Long Delays (days to weeks)

Long delays should always use an external timing store with a periodic scheduled monitor. No automation platform’s execution context is designed to persist for that duration.

Delayed execution also requires careful handling of the context that the delayed action needs when it fires. A Wait node preserves the full execution context automatically the data that was present when the Wait node was reached is still present when it exits, because the execution was never terminated. An external timing store pattern does not preserve execution context; the scheduled monitor that detects the delay expiry must re-query all relevant context from external systems (HubSpot, Slack, etc.) at the moment of execution. This re-query is an additional failure point the Contact’s state in HubSpot may have changed during the delay which is why the scheduled monitor pattern includes stop conditions: if the Contact’s state indicates that the follow-up is no longer relevant, the scheduled monitor does not proceed.

Understanding delayed execution is essential for any automation engineer building systems that have temporal behavior. Most automation platforms n8n, Zapier, Make, Temporal, and their equivalents provide some form of delayed execution, but their implementations differ in their resource model, persistence guarantees, and maximum delay duration. An engineer who understands the resource model of Wait nodes and the persistence model of scheduled monitors can evaluate any platform’s delayed execution capabilities and select the appropriate pattern for a given use case.

Delayed execution also introduces a class of race conditions that do not exist in immediate-execution workflows. If an intake event and a follow-up event for the same Contact occur close in time a Contact’s status is updated in HubSpot at the same moment that Workflow C’s scheduled execution reaches that Contact the two events may produce conflicting writes to the Contact’s properties. The stop conditions in Workflow C are designed to handle the most common version of this race condition (if Task is complete, skip), but the general problem of concurrent access to a shared Contact record is a design concern that applies to all scheduled workflows that operate on records that are also modified by event-triggered workflows.

The brokerage’s system uses the external timing store pattern for all follow-up delays, storing the relevant timing metadata in HubSpot Contact properties. This choice was made for three reasons: the delays range from 2 hours to 30 days (spanning the range where Wait nodes become impractical), the timing metadata needs to be visible and editable by the operations team in HubSpot without requiring access to n8n, and the scheduled monitor pattern’s ability to batch-process all overdue Contacts in a single execution is more efficient than running one persistent execution per Contact.

The key timing metadata properties written by Workflow A to each Contact record are: followup_task_created_at (ISO 8601 timestamp of the initial follow-up Task creation the anchor for all relative interval calculations), followup_schedule_tier (copy of priority_label at intake time, stored separately so a later priority label change does not alter the follow-up schedule for in-progress sequences), followup_tp1_due_at (business-hours-adjusted timestamp for Touchpoint 1, pre-computed by Workflow A at intake time), followup_tp2_due_at (business-hours-adjusted timestamp for Touchpoint 2), followup_esc_due_at (business-hours-adjusted timestamp for escalation), followup_tp1_sent_at (timestamp of actual Touchpoint 1 delivery, written by Workflow C when a reminder is sent, null if not yet sent), followup_tp2_sent_at (timestamp of actual Touchpoint 2 delivery), escalation_triggered (boolean, set true when escalation fires), and escalation_triggered_at (timestamp of escalation event).

Pre-computing the due timestamps at intake time (rather than computing them at poll time) shifts the business-hours utility function execution to Workflow A, where it runs once per Contact. This reduces Workflow C’s per-Contact computation and makes the due timestamps directly visible in HubSpot, which the operations team can inspect and override if needed.

The operations team override case is significant: if a broker is on vacation and the operations director wants to extend a Hot lead’s follow-up window, they can update followup_tp1_due_at and followup_tp2_due_at directly in HubSpot. Workflow C reads these properties on each polling cycle, so the override takes effect within 30 minutes without any workflow changes.

The pre-computed due timestamps introduce a subtle design question: what happens if Workflow A’s business hours utility function is called at 11pm on a Saturday for a Hot lead whose Touchpoint 1 should fire “2 business hours after creation”? The utility function should advance to 8am Monday (the next business hours window), then add 2 business hours, producing a due timestamp of 10am Monday. The utility function must not simply add 2 hours to the current wall-clock time (which would produce 1am Sunday meaningless and inaccessible for operational purposes).

There is also a secondary edge case: what if Workflow A is executing a Contact at exactly the polling boundary for example, at 8:29am, one minute before Workflow C’s next execution at 8:30am? The pre-computed followup_tp1_due_at for a Hot lead with a 2-hour interval would be 10:29am. Workflow C at 8:30am would check current_time > followup_tp1_due_at and find false the reminder should not yet fire. This is correct behavior. The next poll at 9:00am would also find false (10:29am is still in the future). The poll at 10:30am would find true, triggering the Touchpoint 1 reminder approximately 1 minute after the due timestamp. This is the expected behavior given the 30-minute polling interval.

CautionProduction Risk

Using Wait nodes for delays measured in days or weeks is the most consequential delayed execution design mistake. An n8n Wait node that holds an execution open for 14 days consumes an active execution slot for 14 days. In an instance with a concurrency limit of 5 active executions, three concurrent 14-day Wait nodes leave only 2 slots for all other automation. This resource contention is invisible until it causes intake workflows to queue which may happen precisely when lead volume is highest and the system is under the most demand. Delays longer than a few hours should always use an external timing store with a scheduled monitor.

CautionProduction Risk

Not accounting for Contact state changes during a delay can produce incorrect follow-up behavior. A Wait node that was entered when a Contact had priority_label = "Hot" and exits 2 hours later when the Contact has been manually reclassified to priority_label = "Warm" will apply Hot-tier follow-up behavior to a Contact who has been explicitly reclassified. The external timing store pattern avoids this by re-reading the Contact’s current state from HubSpot at execution time.

CautionProduction Risk

The business hours utility function must be tested against at least six edge cases before deployment: end-of-day, after-hours, Friday afternoon, weekend, day before a holiday, and the holiday itself. “2 business hours after 4:30pm on a Friday” should produce “10:30am Monday” not “6:30pm Friday” (outside hours), “4:30am Sunday” (weekend), or a null value. Untested edge cases in timing functions tend to surface in production at exactly the wrong moment when a lead submits on a holiday weekend and the follow-up fires at 3am.


2.6.4 Retry and Escalation Logic

Technical Failure

A technical failure is a system-level failure: an API call returns a 5xx error, a Slack message fails to deliver, a HubSpot write is rejected due to a rate limit.

Business Non-Response

A business non-response is a behavior-level failure: a broker received a notification but did not act on it, a lead did not respond to an outreach attempt, a Task was created but not completed within the SLA.

These two failure types require fundamentally different responses conflating them produces automation that misapplies the retry mechanism designed for one type to the other.

Retry and escalation logic addresses the inevitable gap between what the automation system expects to happen and what actually happens. These two failure types require different handling strategies, and conflating them produces automation that misapplies the retry mechanism designed for one type to the other.

Technical failure retry follows established distributed systems patterns: exponential backoff with a maximum retry count, logging of each retry attempt, and a defined terminal state (dead-letter behavior) when all retries are exhausted. The assumption is that the failure is transient caused by a temporary API unavailability, a network issue, or a rate limit and that retrying after a brief delay will succeed. Technical failures should be handled at the node level (n8n’s built-in retry configuration) or at the workflow level (an error branch that attempts a secondary delivery path before logging the failure).

Business non-response handling is a behavioral escalation process, not a technical retry. The assumption is that the notification was successfully delivered but the recipient did not act on it. The response is not to resend the identical notification (which the recipient presumably saw and chose not to act on immediately), but to send a different communication with different urgency, through a different channel, or to a different recipient that creates a new decision point for the relevant human. The escalation path progressively increases the urgency and organizational visibility of the unaddressed lead until a human takes action or the escalation reaches its terminal state (a new Task assigned to the operations manager).

Mixing technical retry and business escalation into a single mechanism produces a system that behaves incorrectly in both failure types. A system that retries a broker notification 3 times in 5 seconds because the Slack API returned a 503 error is correctly handling a technical failure. The same system that retries the broker notification 3 times in 5 seconds because the broker did not respond immediately is sending spam to the broker’s Slack channel and training the broker to ignore multiple notifications. The retry mechanism must distinguish between “the message was not delivered” and “the message was delivered but not acted upon.”

The escalation path also has a specific organizational function in the brokerage context. A follow-up sequence that escalates to the operations manager is not punishing the broker it is activating a management visibility layer that exists precisely to ensure high-value leads are not lost due to individual oversight. The operations manager’s involvement is a feature of the design, not a failure mode. The automation engineer should frame escalation as “expanding the organizational response” rather than “detecting a broker failure.”

Retry and escalation logic also requires explicit terminal states. An escalation sequence that has no defined endpoint that continues escalating indefinitely for every unanswered lead will eventually saturate the operations manager’s attention with alerts for leads that are genuinely not worth pursuing. Terminal states the escalation Task was created, the operations manager was notified define the point at which the automation has done everything it can and human judgment must take over.

The brokerage’s retry and escalation logic has two parallel tracks that run independently.

Technical Failure Track

Technical failure track (API and delivery failures): Every HTTP Request node in Workflow C is configured with n8n’s native retry settings: 3 retries with exponential backoff starting at 1 second (1s, 2s, 4s). If all retries fail, the node is configured to continue on failure with an error flag set. A downstream Code node checks for error flags and, if any node failed after all retries, routes to the error path: a Slack message to #crm-ops-errors with the full failure context (which Contact, which action, what error code), and a HubSpot property write to last_automation_error and last_automation_error_at on the Contact record. The Contact is not reclassified; the failed action is logged and a human is alerted.

Business Non-Response Track

Business non-response track (broker inaction escalation): This is Workflow C’s primary function. The logic is a priority-aware state machine with three states per Contact: pending_tp1 (Task created, no reminder sent), pending_tp2 (TP1 sent, second reminder not yet due or sent), pending_escalation (TP2 sent, escalation not yet due or triggered).

Workflow C evaluates each Contact against the current time. If current_time > followup_tp1_due_at AND followup_tp1_sent_at is null AND Task not complete: send TP1 reminder and write followup_tp1_sent_at = now(). If current_time > followup_tp2_due_at AND followup_tp2_sent_at is null AND Task not complete: send TP2 reminder and write followup_tp2_sent_at = now(). If current_time > followup_esc_due_at AND escalation_triggered = false AND Task not complete: execute escalation sequence and write escalation_triggered = true, escalation_triggered_at = now().

Each state transition includes a check for the stop conditions defined in the Follow-up Timing Architecture (Chapter 2.6.1): Task complete, Contact disqualified, escalation already triggered. These checks prevent the state machine from firing redundant actions when two consecutive Workflow C executions catch the same Contact in an overdue state.

Escalation sequence: When a Contact reaches the escalation state, Workflow C: (1) creates a new HubSpot Task with subject “ESCALATED: No outreach to [Contact Name] review required” assigned to DEFAULT_OPS_MANAGER_ID (n8n environment variable), due immediately; (2) sends a structured Slack message to #crm-ops-escalations with the Contact’s name, company, priority label, combined score, original assignment, and elapsed time since Task creation; (3) writes escalation_triggered = true and escalation_triggered_at = now() to the Contact record; (4) writes an escalation entry to the Contact’s structured Note (via a new Note body entry, not overwriting the original intake Note); (5) sets escalation_triggered = true as the terminal state no further automated follow-up actions will fire for this Contact until a human clears the flag.

The escalation state machine stored in HubSpot properties is Workflow C’s external state representation. n8n workflows are stateless between executions when a Workflow C execution completes at 8:30am, it retains no memory of which Contacts it evaluated. When it runs again at 9:00am, it must re-derive the state for every overdue Contact from HubSpot properties. The followup_tp1_sent_at, followup_tp2_sent_at, escalation_triggered, and related properties are not just audit fields they are the state machine’s persistent state representation that allows a stateless executor to produce stateful behavior.

This design has a specific advantage: the state is visible and editable in HubSpot. An operations manager who wants to reset an escalation flag and restart the follow-up sequence for a Contact can do so by clearing escalation_triggered, escalation_triggered_at, followup_tp1_sent_at, and followup_tp2_sent_at in HubSpot. Workflow C will then evaluate the Contact as being in the pending_tp1 state on its next execution and proceed accordingly. This manual override capability is only possible because the state is externalised from the workflow execution context.


Diagram 2.6.2 Retry and Escalation Flow

The two parallel tracks technical failure retry and business non-response escalation are shown in Figure 23.2.

%%{init:{"theme":"base","themeVariables":{"primaryColor":"#eef2ff","primaryTextColor":"#1e1b4b","primaryBorderColor":"#6366f1","lineColor":"#6366f1","clusterBkg":"#f8f9ff","clusterBorder":"#6366f1","titleColor":"#1e1b4b","edgeLabelBackground":"#f6f4ef","fontFamily":"system-ui,sans-serif","fontSize":"13px"}}}%%

flowchart TD
    subgraph TF["Technical Failure Track (API errors, delivery)"]
        T1["HTTP Request fails"]:::process --> T2["n8n retry x3, exponential: 1s, 2s, 4s"]:::fallback
        T2 -->|"success"| T3(["Continue"]):::process
        T2 -->|"all retries fail"| T4["Error branch"]:::fallback
        T4 --> T5["Log to #crm-ops-errors; write last_automation_error, last_automation_error_at"]:::process
        T5 --> T6(["TERMINAL human review"]):::fallback
    end

    subgraph BN["Business Non-Response Track (broker inaction)"]
        B1["Workflow C polls every 30 min"]:::process --> B2["Load Contact from HubSpot (overdue Task candidates)"]:::process
        B2 --> B3{"Stop conditions: Task complete? Escalated? Disqualified? lifecycle=customer?"}:::decision
        B3 -->|"any true"| B4(["SKIP"]):::process
        B3 -->|"none true"| B5{"Evaluate state vs. due timestamps"}:::decision
        B5 -->|"pending_tp1 (tp1 not sent, past due)"| B6["Send TP1 reminder; write tp1_sent_at"]:::process
        B5 -->|"pending_tp2 (tp2 not sent, past due)"| B7["Send TP2 reminder; write tp2_sent_at"]:::process
        B5 -->|"pending_escalation (escalation past due)"| B8["Execute escalation: 1. Create Ops Task 2. Slack #crm-ops-escalations 3. Write escalation_triggered=true 4. Write escalation Note entry"]:::success
        B8 --> B9(["TERMINAL human takes over"]):::process
    end
    classDef trigger  fill:#dcfce7,stroke:#16a34a,color:#14532d,font-weight:600
    classDef process  fill:#eef2ff,stroke:#6366f1,color:#1e1b4b
    classDef decision fill:#fef9c3,stroke:#ca8a04,color:#78350f,font-weight:600
    classDef success  fill:#d1fae5,stroke:#059669,color:#064e3b,font-weight:700
    classDef fallback fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
Figure 23.2: Retry and Escalation Flow. Retry and Escalation Flow: technical failures use exponential-backoff retry; business non-response uses Workflow C’s three-tier state machine.

State machine properties stored in HubSpot per Contact:

Property Meaning
followup_tp1_sent_at NULL → state pending_tp1
followup_tp2_sent_at NULL → state pending_tp2 (after TP1 sent)
escalation_triggered false / true
escalation_triggered_at NULL or timestamp
last_automation_error NULL or error description

CautionProduction Risk

Not defining terminal states is the most consequential escalation design mistake. A system that can trigger a new escalation notification on every polling cycle because there is no escalation_triggered flag that prevents re-escalation will flood the operations manager’s Slack channel with duplicate alerts for every overdue Contact, every 30 minutes, until someone manually deletes the Contact’s Task. Terminal states are not optional; they are what converts an escalation loop into an escalation event.

CautionProduction Risk

Sending the same notification at every retry touchpoint undermines the escalation’s behavioral purpose. Touchpoint 2 should differ in urgency framing, not just in timing. A Touchpoint 2 that is identical to Touchpoint 1 has no additional persuasive value the broker already saw Touchpoint 1 and did not act. The escalating urgency framing (“this lead has now been uncontacted for 6 hours” vs. “this lead has not been contacted yet”) communicates a different message and creates a stronger behavioral nudge.

CautionProduction Risk

Escalation notifications addressed to generic team channels rather than a specific individual with defined responsibility diffuse accountability. A message to #sales-team or #general means everyone assumes someone else will handle it. Escalation messages must be addressed to a named individual (the operations manager, a sales director) or must @-mention that individual explicitly in the team channel message. Including elapsed time and original assignment information routes accountability appropriately: “Hot lead not contacted 24 hours since initial notification: [Contact Name] at [Company]. Score: 23/28. Original assignment: [Broker Name] at [timestamp].”


Practical Exercise 2.6 Follow-Up Monitor

The four concepts introduced in Sections 2.6.1 through 2.6.4 follow-up strategy design, time-based triggers, delayed execution control, and retry and escalation logic define the complete timing and follow-up architecture. The Chapter 2.6 Practical Implementation introduces Workflow C: the Follow-up Monitor, a scheduled workflow that runs every 30 minutes during business hours, evaluates the follow-up state of active Contacts, and executes the priority-aware reminder and escalation sequence defined in the follow-up strategy. Workflow A is extended to pre-compute and write the follow-up timing metadata properties needed by Workflow C. Workflow B is unchanged.

Business Scenario

The brokerage’s Workflows A and B classify and notify on every lead. Brokers receive a Slack alert and a Task assignment within seconds of submission. Despite this, the operations director has measured a 34% Hot lead SLA compliance rate 66% of Hot leads miss the 2-hour callback window. Commercial real estate leads that go uncontacted within 24–48 hours have a significantly lower conversion rate than those contacted within SLA.

The Problem

The brokerage’s intake automation (Workflows A and B from Sections 5.1–5.5) reliably classifies, scores, and notifies on new leads. However, the system has no mechanism to verify that the initial broker notification was acted upon, enforce the SLA defined in the priority mapping (Chapter 2.5.6), remind brokers who have not yet followed up, or escalate inactivity to management. In production, the Hot lead SLA compliance rate is 34% meaning that 66% of Hot leads do not receive a first callback within the 2-hour SLA window. Leads that miss their follow-up window have a significantly lower conversion rate than leads contacted within SLA.

The absence of a follow-up enforcement layer means that the scoring and routing automation built in Sections 5.3–5.5 is delivering correct classification but incomplete operational impact. The missing layer must monitor Task completion status, apply a priority-aware reminder cadence, and escalate persistent inactivity to management all without human intervention.

Proposed Solution

Three changes are made to the existing architecture.

Workflow A extension: After the Task creation and association steps, a new Code node computes the pre-adjusted follow-up due timestamps using the business hours utility function. A subsequent HTTP Request node writes the new follow-up metadata properties to the Contact record using a HubSpot property update call. The existing batch upsert already includes the Contact’s followup_schedule_tier (a copy of priority_label); the new write adds followup_task_created_at, followup_tp1_due_at, followup_tp2_due_at, and followup_esc_due_at.

Workflow C: Follow-up Monitor: A new workflow with a Schedule Trigger node set to every 30 minutes during business hours. On each execution: queries HubSpot for Contacts with active follow-up sequences (active = followup_task_created_at is not null AND escalation_triggered = false AND Task not in completed/deleted status), evaluates each Contact’s state against the current time, and sends the appropriate reminder or escalation notification. Workflow C performs a batch query rather than a per-Contact execution: it retrieves up to 100 overdue Contacts per execution (HubSpot’s POST /crm/v3/objects/contacts/search endpoint with a filter on followup_tp1_due_at ≤ now AND followup_tp1_sent_at = null, plus equivalent queries for TP2 and escalation), processes each in an n8n Loop Over Items node, and writes state updates back to HubSpot for each processed Contact.

HubSpot custom properties (new): Ten new Contact properties support the follow-up state machine. These are created once in HubSpot and written/read by Workflows A and C.

Limited-Scope Workflow

Extended Workflow A with Follow-up Timing Writes

Steps 1–24 from Chapter 2.5 are structurally unchanged (through Note creation, association, and Slack notification).

Step 25 Compute Follow-up Due Timestamps

Purpose

Pre-computing the follow-up due timestamps at intake time rather than at Workflow C poll time ensures that the business hours calculation runs exactly once per Contact, that the resulting timestamps are stored in HubSpot where the operations team can inspect and override them directly, and that Workflow C’s per-Contact computation is reduced to a simple timestamp comparison rather than a business hours calculation on every polling cycle.


Operation Summary

Property Value
Node Type Code (JavaScript)
Input priority_label, Task creation timestamp from execution context
Primary Function Load tier configuration; apply business hours utility function to compute three due timestamps
Output followup_tp1_due_at, followup_tp2_due_at, followup_esc_due_at (ISO 8601 UTC)

Implementation Logic

const tier = item.priority_label; // "Hot" | "Warm" | "Cool" | "Cold"
const config = JSON.parse(process.env[`FOLLOWUP_CONFIG_${tier.toUpperCase()}`]);
const anchor = new Date(item.task_created_at);

// Business hours utility: advances anchor + interval to next
// valid business hours window in BROKERAGE_TIMEZONE
const tp1_due = nextBusinessHours(anchor, config.tp1_hours  ?? config.tp1_days * 24);
const tp2_due = nextBusinessHours(anchor, config.tp2_hours  ?? config.tp2_days * 24);
const esc_due = nextBusinessHours(anchor, config.esc_hours  ?? config.esc_days * 24);

The business hours utility function implemented as a reusable helper in this Code node converts the anchor timestamp to the brokerage’s local timezone (BROKERAGE_TIMEZONE env variable), applies the interval, advances to the next valid business hours window (8am–8pm, Monday–Friday, excluding holidays in BROKERAGE_HOLIDAYS env variable), then converts back to UTC for storage.


Response Processing

return [{
  json: {
    ...item,
    followup_tp1_due_at: tp1_due.toISOString(),
    followup_tp2_due_at: tp2_due.toISOString(),
    followup_esc_due_at: esc_due.toISOString()
  }
}];

All three timestamps are written as ISO 8601 UTC strings. The business hours utility must be tested against at least six edge cases before deployment: end-of-business-day, after-hours, Friday afternoon (→ Monday morning), weekend anchor, pre-holiday, and holiday itself.


Output Table

Output Description
followup_tp1_due_at ISO 8601 UTC; business-hours-adjusted Touchpoint 1 due time
followup_tp2_due_at ISO 8601 UTC; business-hours-adjusted Touchpoint 2 due time
followup_esc_due_at ISO 8601 UTC; business-hours-adjusted escalation due time

Engineering Rationale

NoteEngineering Rationale

Pre-computing due timestamps at intake time makes the follow-up schedule human-readable and editable in HubSpot without requiring n8n access. A broker vacation extension is a HubSpot property edit that takes effect on the next Workflow C polling cycle. If timestamps were computed at poll time, there would be nothing to inspect or override the schedule would be locked inside Workflow C’s execution logic.


Step 26 Write Follow-up Metadata to Contact

Purpose

The follow-up timing metadata must be written to HubSpot as a separate PATCH call not bundled into the batch upsert so that failures in this non-critical write do not affect the Contact creation and core scoring property writes that are the intake workflow’s primary output. A failed follow-up metadata write degrades the follow-up system; a failed batch upsert means the Contact was never created.


Operation Summary

Property Value
Node Type HTTP Request
Method PATCH
Endpoint /crm/v3/objects/contacts/{contactId}/properties
Primary Function Write five follow-up metadata properties to the Contact record
Input contactId from batch upsert response; all three due timestamps from Step 25
Output Updated Contact object (200); failure logged and execution continues

Request Payload

{
  "properties": {
    "followup_task_created_at": "{{$json.task_created_at}}",
    "followup_schedule_tier":   "{{$json.priority_label}}",
    "followup_tp1_due_at":      "{{$json.followup_tp1_due_at}}",
    "followup_tp2_due_at":      "{{$json.followup_tp2_due_at}}",
    "followup_esc_due_at":      "{{$json.followup_esc_due_at}}"
  }
}

followup_schedule_tier is a copy of priority_label stored at intake time as a separate property. This prevents a later priority label change for example, a manual re-score from retroactively altering the follow-up schedule for sequences already in progress.

Request Field Table

Field Required Description
followup_task_created_at Yes Anchor timestamp for all relative interval calculations
followup_schedule_tier Yes Copy of priority_label at intake time
followup_tp1_due_at Yes Pre-computed Touchpoint 1 due timestamp
followup_tp2_due_at Yes Pre-computed Touchpoint 2 due timestamp
followup_esc_due_at Yes Pre-computed escalation due timestamp

Response Processing

// Continue on Fail: enabled
// On HTTP error: log to existing error handler; workflow continues
// The follow-up metadata write is non-blocking its failure does
// not prevent the Contact record, Task, Note, or Slack notification
// from completing successfully.

The node is configured with Continue on Fail: true. A failure here produces a Contact without follow-up timestamps, which Workflow C will not pick up for reminders a degraded but recoverable state. The error is logged to the execution summary and surfaced in the ops error channel.


Output Table

Output Description
200 OK Contact updated; all five properties written
error Failure logged; execution continues without blocking intake

Engineering Rationale

NoteEngineering Rationale

Isolating the follow-up metadata write as a separate non-blocking PATCH call from the batch upsert preserves the intake-critical path’s reliability. A transient HubSpot API rate-limit error at the moment of metadata write does not fail the Contact record creation, the Task, the Note, or the Slack notification. The Contact may miss its first follow-up reminder, but the intake record is complete and auditable.

Workflow C: Follow-up Monitor (New)

Step 1 Schedule Trigger (Workflow C)

Purpose

Workflow C must run frequently enough to detect SLA breaches near their threshold a Hot lead’s 2-hour SLA requires sub-30-minute detection latency. The Schedule Trigger provides the wall-clock firing mechanism. All per-Contact timing logic is in subsequent steps; this node’s only responsibility is to start the execution at the configured interval.


Operation Summary

Property Value
Node Type Schedule Trigger
Cron Expression 0,30 8-20 * * *
Schedule Every 30 minutes, 8am–8pm, every day of the week
Primary Function Entry point for all follow-up monitoring executions
Output Execution start; no data payload

Engineering Rationale

NoteEngineering Rationale

The 30-minute polling interval means that a 2-hour Hot lead SLA breach will be detected within 30 minutes of occurring a maximum overrun of 25%. This is acceptable for a 2-hour SLA but would not be acceptable for a 30-minute SLA. If the brokerage’s Hot tier SLA is later tightened, the polling interval must be reduced proportionally. Polling interval and SLA duration must be designed together, not independently.


Step 2 Load Current Time and Compute Business Hours Check

Purpose

Cron expressions do not account for daylight saving time transitions. At a DST boundary, the scheduler may fire one hour early or late relative to local time, potentially producing broker alerts at 7am or 9pm. This Code node checks the actual local time in the brokerage’s configured timezone and sets a skip flag if the execution falls outside the business hours window, providing a defense-in-depth backstop independent of the cron expression’s accuracy.


Operation Summary

Property Value
Node Type Code (JavaScript)
Input None (reads environment variables)
Primary Function Load current UTC time; convert to brokerage timezone; evaluate business hours window
Output current_time_utc, current_time_local, skip_execution

Implementation Logic

const now = new Date();
const tz   = process.env.BROKERAGE_TIMEZONE; // e.g. "America/New_York"

const localHour = parseInt(
  now.toLocaleString('en-US', { timeZone: tz, hour: 'numeric', hour12: false })
);
const localDay  = now.toLocaleDateString('en-US', { timeZone: tz, weekday: 'long' });

const BIZ_START = 8;
const BIZ_END   = 20;
const weekends  = ['Saturday', 'Sunday'];

const skip_execution =
  localHour < BIZ_START ||
  localHour >= BIZ_END  ||
  weekends.includes(localDay);

Output Table

Output Description
current_time_utc ISO 8601 UTC string for timestamp comparisons
current_time_local Local time string for logging
skip_execution Boolean; true halts the execution immediately

Engineering Rationale

NoteEngineering Rationale

The business hours check in this Code node is a defense-in-depth measure, not redundancy. Cron expressions do not account for daylight saving time transitions: at a DST boundary, the scheduler may fire one hour early or late relative to local time. The Code node checks actual local time and skips execution if outside the business hours window, ensuring that brokers never receive follow-up alerts at 7am or 9pm due to a DST edge case.


Step 3 Business Hours Gate

Purpose

Routes on the skip_execution flag from Step 2. When outside business hours, the workflow terminates immediately without querying HubSpot, preventing the three Contact Search queries in Steps 4–6 from executing and consuming API quota during hours when no action is appropriate.


Operation Summary

Property Value
Node Type IF
Condition skip_execution === false
True branch Continue to HubSpot Contact Search queries
False branch End immediately (no-op execution)

Steps 4–6 Query HubSpot for Overdue Contacts

Purpose

Workflow C issues three parallel HubSpot Contact Search queries one per escalation state (TP1 overdue, TP2 overdue, escalation overdue) rather than a single query, because the filter logic for each state requires different combinations of timestamp and null checks. Parallel execution minimizes total query latency on each polling cycle.


Operation Summary

Property Value
Node Type HTTP Request (×3, executed in parallel)
Method POST
Endpoint https://api.hubapi.com/crm/v3/objects/contacts/search
Primary Function Retrieve Contacts whose follow-up timestamps have passed and whose state properties indicate no action has been taken
Output Three result sets, each an array of Contact objects with timing metadata

Request Payload Step 4 (TP1 Overdue)

{
  "filterGroups": [{
    "filters": [
      { "propertyName": "followup_tp1_due_at",
        "operator": "LTE", "value": "{{current_time_utc}}" },
      { "propertyName": "followup_tp1_sent_at",
        "operator": "NOT_HAS_PROPERTY" },
      { "propertyName": "followup_schedule_tier",
        "operator": "HAS_PROPERTY" }
    ]
  }],
  "properties": ["id","firstname","lastname","email","company",
                 "hubspot_owner_id","priority_label","combined_score",
                 "followup_tp1_due_at","followup_tp2_due_at",
                 "followup_esc_due_at","followup_task_created_at",
                 "followup_schedule_tier","escalation_triggered"],
  "limit": 100
}

Step 5 (TP2 Overdue) replaces followup_tp1_due_at/followup_tp1_sent_at with followup_tp2_due_at + NOT_HAS_PROPERTY followup_tp2_sent_at + HAS_PROPERTY followup_tp1_sent_at. Step 6 (Escalation Overdue) filters on followup_esc_due_at LTE now + escalation_triggered EQ false + HAS_PROPERTY followup_tp2_sent_at.

Request Field Table

Filter Field Operator Purpose
followup_tp1_due_at LTE current_time Threshold has passed
followup_tp1_sent_at NOT_HAS_PROPERTY Reminder not yet sent
followup_schedule_tier HAS_PROPERTY Exclude Contacts without active follow-up schedule

Engineering Rationale

NoteEngineering Rationale

HubSpot’s Search API uses HAS_PROPERTY / NOT_HAS_PROPERTY operators for null checks not IS NULL / IS NOT NULL via EQ/NEQ. Using the wrong operator produces a filter that silently returns no results, causing Workflow C to skip all overdue Contacts without error. This is the most common implementation pitfall for HubSpot Search API integrations.


Step 7 Merge and Deduplicate Contact Sets

Purpose

A Contact may appear in more than one query result if multiple thresholds passed simultaneously for example, if Workflow C failed for several hours and both TP1 and TP2 windows elapsed before the next successful execution. Deduplication ensures each Contact receives exactly one action per polling cycle. The highest-priority-action rule (escalation > TP2 > TP1) ensures that a Contact who has crossed the escalation threshold is not sent a TP1 reminder instead.


Operation Summary

Property Value
Node Type Code (JavaScript)
Input Three arrays of Contact objects from Steps 4–6
Primary Function Merge result sets; deduplicate by Contact ID; annotate each Contact with highest-priority action_type
Output Deduplicated array of Contact objects with action_type field

Implementation Logic

const tp1Contacts = $input.all()[0].json.results ?? [];
const tp2Contacts = $input.all()[1].json.results ?? [];
const escContacts = $input.all()[2].json.results ?? [];

const contactMap = new Map();

// Escalation takes precedence add last to overwrite lower priority
for (const c of tp1Contacts) contactMap.set(c.id, { ...c, action_type: 'tp1_reminder' });
for (const c of tp2Contacts) contactMap.set(c.id, { ...c, action_type: 'tp2_reminder' });
for (const c of escContacts) contactMap.set(c.id, { ...c, action_type: 'escalation' });

return [...contactMap.values()].map(c => ({ json: c }));

Output Table

Output Description
action_type tp1_reminder / tp2_reminder / escalation highest priority
Contact fields All properties requested in Steps 4–6

Engineering Rationale

NoteEngineering Rationale

A Contact who crossed both the TP1 and TP2 thresholds since the last successful execution should receive TP2 behavior a higher-urgency message with escalation warning not TP1. Sending TP1 first and deferring TP2 to the next polling cycle would understate the urgency and add a 30-minute delay to an already-delayed response. The highest-priority-action rule prevents this by ensuring deduplication always resolves toward the most urgent applicable action.


Step 8 Loop Over Items: Process Each Overdue Contact

Purpose

Each overdue Contact requires an individual stop-conditions check, a governance-aware routing decision, and one or more HubSpot writes and Slack sends. The Loop Over Items node processes each Contact sequentially within a single Workflow C execution, ensuring that state writes from one Contact’s processing are visible to subsequent iterations if any Contact appears in multiple action branches.


Operation Summary

Property Value
Node Type Loop Over Items
Input Deduplicated Contact array from Step 7
Primary Function Sequentially evaluate each Contact against stop conditions and action type; execute Slack send and HubSpot state write
Output Per-Contact action result; aggregated in Step 9

Implementation Logic Stop Conditions Check

// HTTP GET: Task association check
// GET /crm/v3/objects/contacts/{id}/associations/tasks
// Then: GET /crm/v3/objects/tasks/{taskId}?properties=hs_task_status

const taskComplete =
  taskStatus === 'COMPLETED' || taskStatus === 'DELETED';
const isCustomer =
  contact.lifecyclestage === 'customer' ||
  contact.lifecyclestage === 'other';   // disqualified

const skip_contact = taskComplete || isCustomer ||
  contact.escalation_triggered === 'true';

If skip_contact = true, no action is taken and no state write occurs. The Contact is counted in contacts_skipped_count in the execution summary.


Implementation Logic Switch on Action Type

// TP1 Reminder
const tp1_message =
  `⚠️ Follow-up Due: ${firstname} ${lastname} at ${company} ` +
  `— ${priority_label} lead. Task due at ${followup_tp1_due_at}. ` +
  `Assigned: ${broker_name}.`;
// → Slack POST to priority-tier channel
// → HubSpot PATCH: followup_tp1_sent_at = now()

// TP2 Reminder
const tp2_message =
  `🔴 OVERDUE Second reminder: ${firstname} ${lastname} at ${company}` +
  ` ${elapsed} without outreach. Escalation in ${remaining} if no action.`;
// → Slack POST
// → HubSpot PATCH: followup_tp2_sent_at = now()

// Escalation
const esc_message =
  `🚨 ESCALATED: ${firstname} ${lastname} at ${company} ` +
  `${elapsed} without outreach. Score: ${combined_score}/28. ` +
  `Originally assigned: ${broker_name} at ${task_created_time}. ` +
  `Action required. <@${OPS_MANAGER_SLACK_ID}>`;
// → HubSpot Create Escalation Task
// → Slack POST to #crm-ops-escalations
// → HubSpot PATCH: escalation_triggered = true, escalation_triggered_at = now()
// → HubSpot Create Escalation Note on Contact

Output Table

Output Description
Slack message sent Channel varies by action_type and priority tier
followup_tp1_sent_at Written to Contact on TP1 action
followup_tp2_sent_at Written to Contact on TP2 action
escalation_triggered Written true to Contact on escalation action
escalation_triggered_at Written timestamp to Contact on escalation action
Escalation Task created Assigned to DEFAULT_OPS_MANAGER_ID, due immediately
Escalation Note on Contact Structured Note entry recording the escalation event

Engineering Rationale

NoteEngineering Rationale

The stop conditions check inside the loop re-fetching Task status from HubSpot per Contact is a deliberate design choice over relying on the query result alone. The HubSpot Search query that identified this Contact as overdue was executed at the start of the polling cycle; in the time between query execution and the loop reaching this Contact, the broker may have completed the Task. The per-iteration status check catches this race condition and suppresses the now-unnecessary reminder.


Steps 9–10 Execution Summary and Error Routing

Purpose

Workflow C runs on a 30-minute schedule, potentially processing dozens of Contacts per execution. Sending an execution summary only when errors occur keeps the ops monitoring channel signal-to-noise ratio high. Silent success is appropriate; silent failure is not.


Operation Summary

Property Value
Node Type Code (Step 9) + IF + HTTP Request (Step 10)
Input Aggregated loop results
Primary Function Aggregate execution counts; route error summary to Slack if any errors occurred
Output Slack error summary to #crm-ops-errors (conditional)

Implementation Logic

// Step 9 Aggregate
const summary = {
  tp1_sent_count:            loop_results.filter(r => r.action === 'tp1_sent').length,
  tp2_sent_count:            loop_results.filter(r => r.action === 'tp2_sent').length,
  escalations_triggered:     loop_results.filter(r => r.action === 'escalation').length,
  contacts_skipped_count:    loop_results.filter(r => r.skipped).length,
  errors_count:              loop_results.filter(r => r.error).length
};

Step 10 routes on errors_count > 0. If errors exist, an HTTP Request sends the error summary to #crm-ops-errors with each failed Contact’s name, ID, action type, and error code. If no errors, the workflow ends silently.


Output Table

Output Description
tp1_sent_count Number of TP1 reminders sent in this execution
tp2_sent_count Number of TP2 reminders sent
escalations_triggered Number of escalation sequences fired
contacts_skipped_count Number skipped due to stop conditions
errors_count Number of per-Contact processing failures
Slack error summary Sent to #crm-ops-errors only when errors_count > 0

Engineering Rationale

NoteEngineering Rationale

Execution summaries belong in the ops monitoring channel, not in the broker notification channel. A Workflow C that sends a summary to #sales-alerts after every polling cycle even when no action was taken produces notification fatigue and trains brokers to ignore messages from that channel. Errors route to #crm-ops-errors so the operations team can detect and investigate; successful no-action executions produce no output.

Technologies Used

Core External APIs / Systems

HubSpot Contacts API Property PATCH - Purpose: Write follow-up timing metadata properties to each Contact after intake (Workflow A) and update follow-up state properties after each reminder or escalation event (Workflow C). - Endpoint: PATCH https://api.hubapi.com/crm/v3/objects/contacts/{contactId}/properties - Documentation: https://developers.hubspot.com/docs/api/crm/contacts - Required Operations: Authenticated PATCH with Authorization: Bearer {HUBSPOT_API_KEY} header. Request body: { "properties": { "property_name": "value", ... } }. Returns 200 with updated Contact object on success; 404 if Contact ID not found; 400 if property name is unrecognized (check HubSpot property internal names carefully they are case-sensitive and underscore-delimited by convention). - External System Preparation: Create 10 new HubSpot custom Contact properties before running Workflow A or Workflow C for the first time. Navigate to Settings → Properties → Contacts → Create Property for each: followup_task_created_at (datetime), followup_schedule_tier (single-line text), followup_tp1_due_at (datetime), followup_tp2_due_at (datetime), followup_esc_due_at (datetime), followup_tp1_sent_at (datetime), followup_tp2_sent_at (datetime), escalation_triggered (single checkbox), escalation_triggered_at (datetime), last_automation_error (multi-line text). Add all ten to the Contact record view and to a new “Follow-up Monitoring” property group for operational visibility.

HubSpot Contacts Search API - Purpose: Workflow C’s batch query mechanism for identifying Contacts with overdue follow-up thresholds. Allows complex filter expressions including date comparisons and null-value checks. - Endpoint: POST https://api.hubapi.com/crm/v3/objects/contacts/search - Required Operations: POST with filter groups containing propertyName, operator, and value fields. Date comparison operators: LTE (less than or equal), GTE, EQ, HAS_PROPERTY, NOT_HAS_PROPERTY. Limit up to 100 per request. For null checks: NOT_HAS_PROPERTY for “is null” and HAS_PROPERTY for “is not null”. Note: HubSpot’s Search API does not support IS NULL/IS NOT NULL using the EQ/NEQ operators use HAS_PROPERTY / NOT_HAS_PROPERTY operators instead. This is a common implementation pitfall.

HubSpot Tasks API (Associations) - Purpose: Workflow C checks whether the initial follow-up Task for each Contact has been completed before sending reminders or executing escalation. Task status is retrieved via the associations endpoint. - Endpoint: GET https://api.hubapi.com/crm/v3/objects/contacts/{contactId}/associations/tasks - Required Operations: GET request returns associated Task IDs. For each Task ID, retrieve Task details: GET https://api.hubapi.com/crm/v3/objects/tasks/{taskId}?properties=hs_task_status,hs_task_subject. Check hs_task_status: COMPLETED (completed), DEFERRED (snoozed), IN_PROGRESS (active), NOT_STARTED (not yet started). Reminders should only fire if the initial intake Task (identified by subject prefix “PRIORITY” or “Follow up” from Chapter 2.5) is in NOT_STARTED or IN_PROGRESS status.

Slack Web API chat.postMessage - Purpose: Reminder notifications for TP1, TP2, and escalation events, routed to the appropriate channel based on action type and priority tier. - Endpoint: POST https://slack.com/api/chat.postMessage - Channels used (new in Chapter 2.6): #crm-ops-escalations dedicated escalation alert channel (create and add Slack app if not already present). All other channels (#sales-alerts-hot, #sales-alerts, #crm-ops-intake, #crm-ops-errors) were established in earlier sections. - External System Preparation: Create #crm-ops-escalations channel and add the brokerage’s Slack app. Store the channel ID as n8n environment variable SLACK_ESCALATIONS_CHANNEL. Store the operations manager’s Slack user ID as OPS_MANAGER_SLACK_ID (format: U01XXXXXXX) for @-mention in escalation messages.

Key n8n Nodes

Workflow A additions (Steps 25–26): - Code (×1, NEW) → Business hours utility function; computes three follow-up due timestamps from priority tier configuration. - HTTP Request (×1, NEW) → HubSpot Contact PATCH to write follow-up metadata properties.

Workflow C new workflow: - Schedule Trigger (×1, NEW) → Every 30 minutes, 8am–8pm, 7 days. - Code (×1, NEW) → Business hours safety check; computes query parameters. - IF (×1, NEW) → Skip if outside business hours. - HTTP Request (×3, NEW) → HubSpot Contact Search queries for TP1 overdue, TP2 overdue, escalation overdue. - Code (×1, NEW) → Merge + deduplicate Contact sets; annotate with action_type. - Loop Over Items (×1, NEW) → Iterates over overdue Contact array. - HTTP Request (×1, NEW inside loop) → HubSpot Task association + status check (stop conditions). - IF (×1, NEW inside loop) → Skip branch if stop conditions met. - Switch (×1, NEW inside loop) → Routes on action_type. - Code (×3, NEW inside loop branches) → Build TP1, TP2, and escalation Slack message payloads. - HTTP Request (×5, NEW inside loop branches) → Slack chat.postMessage (×3 branches); HubSpot Contact PATCH for state writes (per branch); HubSpot Task Create (escalation branch); HubSpot Note Create + Associate (escalation branch). - Code (×1, NEW after loop) → Aggregate execution summary. - IF (×1, NEW) → Error summary routing. - HTTP Request (×1, NEW, conditional) → Slack error summary to #crm-ops-errors.

Scope Boundary

The Chapter 2.6 extension introduces the follow-up monitoring and escalation layer within a Slack-and-HubSpot-only architecture. It does not address email follow-up delivery the reminder and escalation notifications in Chapter 2.6 are Slack-only (internal operations notifications to brokers and the operations manager); automated outbound email to the lead requires an outbound email delivery service and is introduced in Chapter 2.7. It does not address omnichannel lead outreach sequencing Chapter 2.6 sends internal notifications only; multi-channel automated outreach to the lead directly is a marketing automation concern introduced in Chapter 2.7. Campaign management and enrollment Cool-tier Contacts are flagged but not automatically enrolled in a marketing automation campaign; sequence enrollment via HubSpot Sequences or equivalent is introduced in Chapter 2.7. Human approval workflow for escalated leads when escalation fires and a new Task is created for the operations manager, the workflow does not wait for approval before proceeding; a human-in-the-loop approval pattern is introduced in Chapter 2.8. Workflow C concurrency handling if Workflow C’s execution takes longer than 30 minutes, the next Schedule Trigger execution may begin while the previous is still running, potentially creating duplicate reminder sends; concurrency locking is deferred to Chapter 2.9. SLA compliance reporting the follow-up audit data enables SLA compliance reports but Chapter 2.6 does not build those reports; HubSpot report construction is addressed in Chapter 2.9.

NoteEngineering Rationale

This implementation omits email follow-up delivery to leads, omnichannel outreach sequencing, marketing automation enrollment, human approval workflows for escalated leads, and Workflow C concurrency handling. The external timing store pattern and the HubSpot property state machine built here are the prerequisite infrastructure for all of these capabilities. Chapter 2.7 extends the architecture with the communication governance layer before outbound communication is introduced.

Chapter 2.7 introduces outbound lead communication and marketing automation integration the direct complement to Chapter 2.6’s inbound broker notification and follow-up enforcement layer.


The three workflows execute independently but share HubSpot as the common state store and source of truth: Workflow A writes all contact and follow-up metadata properties; Workflow B reads from HubSpot’s lifecyclestage change webhooks; Workflow C reads follow-up metadata, writes state properties back, creates escalation Tasks, and sends Slack reminders.


Discussion Questions

1. Why does a follow-up strategy need to distinguish between contact priority levels rather than treating all active contacts identically?

2. A contact has a follow-up task scheduled for 2:00 PM. Your follow-up monitor runs at 2:30 PM but skips the contact because a broker marked a different task complete at 2:05 PM. Is this correct behavior? What business rule would produce it, and is it the right rule?

3. Your follow-up monitor runs every 30 minutes. A contact receives an escalation alert at 3:00 PM. The broker resolves the issue at 3:10 PM but does not update the contact record. At 3:30 PM the monitor runs again. Describe exactly what the monitor checks and what it does.

Chapter Summary

Chapter 2.6 introduced timing as a first-class architectural concern by building the follow-up enforcement layer that the brokerage’s intake system had been missing. The section established that successful automation is not only about making the correct decision at intake, but about ensuring that decision is acted upon within the SLA window and about recovering systematically when it is not. The follow-up strategy, time-based triggers, delayed execution control, and retry and escalation logic form a complete architecture for translating accurate classification into consistent human engagement.

The central design decision in Chapter 2.6 is the external timing store pattern: storing follow-up state as HubSpot Contact properties rather than using n8n Wait nodes for delays ranging from 2 hours to 30 days. This choice makes the follow-up schedule visible and editable in HubSpot, enables Workflow C’s stateless polling architecture to produce stateful per-Contact behavior, and ensures that timing metadata survives n8n restarts, execution context limits, and the full range of delay durations the brokerage’s four priority tiers require. The ten new HubSpot Contact properties introduced in this section are not just audit fields they are the persistent state representation of a state machine that a stateless executor queries on every polling cycle.

Workflow C completes the three-workflow architecture that now spans intake through engagement enforcement. Workflow A handles classification and record creation; Workflow B enforces lifecycle state integrity; Workflow C ensures that the outputs of Workflow A are acted upon within the defined SLA. Together, the three workflows cover the complete intake-to-engagement loop. Chapter 2.7 extends the architecture outward from internal broker notifications to outbound lead communication introducing the marketing automation integration layer that transforms the brokerage’s CRM system from a contact management tool into a full contact-engagement platform.

Transition to Chapter 2.7

Chapter 2.7 builds directly on the system constructed in this chapter.

Key Takeaways

  • A follow-up strategy is the business specification defining touchpoints, intervals, channels, content, and escalation conditions that must be documented before any timing mechanism is implemented. The automation can only handle cases that were anticipated in the strategy.
  • Follow-up strategies are defined at the segment level (per priority tier and lifecycle stage) and stored in configuration objects, making strategy revision a configuration change rather than a workflow edit.
  • Time-based triggers fall into two categories: wall-clock triggers (fire at a fixed calendar time, implemented via Schedule Trigger nodes) and relative triggers (fire a configurable interval after an anchor event, implemented via Wait nodes or external timing stores with scheduled monitors).
  • Delayed execution strategy is determined by delay duration relative to execution context constraints. Short delays use Wait nodes; delays measured in hours to days or weeks should use an external timing store with a scheduled monitor.
  • The brokerage’s system stores all follow-up timing metadata as HubSpot Contact properties pre-computed at intake time by Workflow A giving Workflow C’s stateless polling architecture the persistent state it needs to produce stateful per-Contact follow-up behavior.
  • Business hours awareness requires a utility function that advances interval calculations to the next valid business hours window. The function must be tested against at least six edge cases before deployment: end-of-day, after-hours, Friday afternoon, weekend, pre-holiday, and holiday.
  • Technical failures (message not delivered) and business non-responses (message delivered but not acted upon) require different handling strategies: exponential backoff retry for technical failures, behavioral escalation with escalating urgency for non-responses.
  • The follow-up state machine uses three states per Contact (pending_tp1, pending_tp2, pending_escalation) stored in HubSpot. Terminal states (escalation_triggered = true) are mandatory and prevent repeated escalation notifications.
  • All timing thresholds and configuration values must be externalized as environment variables, not hard-coded into workflow nodes. Polling interval and SLA duration must be designed together to ensure SLA breach detection is timely.
  • Workflow C completes the three-workflow architecture: Workflow A handles intake and classification, Workflow B enforces lifecycle state integrity, and Workflow C enforces follow-up SLA compliance and escalates persistent inactivity to management.

End of Chapter 2.6 Follow-up and Timing Systems