WhatsApp AI Assistant
Module 20 · ai · intermediate
apply.js
// Save the turn: history, lead fields, opt-out and handoff flags. Dry runs save nothing.
const s = $('Settings (edit me)').first().json;
const db = $getWorkflowStaticData('global');
db.convos ??= {};
return $input.all().map(({ json: x }) => {
const c = x.convo;
const now = x.now;
const addHours = (h) => new Date(new Date(now).getTime() + h * 3600e3).toISOString();
let handoff = false, newLead = false;
if (!x.duplicate) {
c.seen = [...c.seen, x.message_id].slice(-50);
c.last_inbound_at = now;
c.window_open_until = addHours(24); // WhatsApp: free-form replies only within 24 h of the customer's last message
c.history.push({ role: 'user', text: x.text || `[${x.type}]`, at: now });
if (x.action === 'opt_out') c.opted_out = true;
if (x.action === 'opt_in') c.opted_out = false;
if (['handoff', 'emergency'].includes(x.action) || x.ai_handoff) { c.handoff_until = addHours(s.handoff_hours); handoff = true; }
if (x.action === 'answer') {
c.lead = { ...c.lead, ...x.lead_patch };
if (!c.lead.name && c.name) c.lead.name = c.name;
c.awaiting = x.next_awaiting ?? null;
const complete = ['service', 'zip', 'name', 'preferred_time'].every((k) => c.lead[k]) && s.service_zips.includes(c.lead.zip);
if (complete && !c.lead_captured_at) { c.lead_captured_at = now; newLead = true; }
}
if (x.reply) c.history.push({ role: 'assistant', text: x.reply, at: now });
c.history = c.history.slice(-40);
c.updated_at = now;
if (!x.dry_run) db.convos[x.wa_id] = c;
}
// Keep static data small: forget conversations idle for 90 days.
if (!x.dry_run) for (const [k, v] of Object.entries(db.convos)) if ((v.updated_at ?? v.created_at) < addHours(-24 * 90)) delete db.convos[k];
const { convo, rules, history_text, ...rest } = x;
return {
json: {
...rest, has_reply: Boolean(x.reply) && !x.duplicate, handoff, new_lead: newLead,
lead: c.lead, lead_complete: Boolean(c.lead_captured_at), awaiting: c.awaiting,
needs_staff: handoff || x.action === 'silent',
},
};
});