[object Object]

A sales manager wants to know which AEs are out today before assigning leads. The data is in Zoho People. The leads land in CRM. Nobody wants to check two tabs. The “obvious” fix is a sync. Six months later you have a Currently_Out checkbox on every User record that’s right 80% of the time and silently wrong the other 20.

Attendance sync is one of those features that looks trivial in a demo and rots in production. Here’s why, and how to do it without rotting.

Why it goes wrong

Four things race against you:

  1. Time zones. A rep based in Mumbai is “in” at 4 AM PST. If your scheduled function runs at midnight PST and reads “today” against the org default zone, you miss the morning shift.
  2. Half-day PTO. People treats half-days differently from full days. A naive sync that checks is_on_leave_today reports half-day reps as fully out.
  3. Late entry. Reps log PTO retroactively. The sync ran yesterday. The record didn’t exist yesterday. The sync doesn’t know to back-fill.
  4. Role visibility. Sharing the Currently_Out field on User records requires careful profile setup, especially with territories. Some users see it, some don’t, nobody notices until a manager misroutes leads.

The pattern below handles the first three. The fourth needs admin discipline.

The right cadence

Don’t sync once a day. Sync every 30 minutes during business hours, plus an immediate webhook from People on any leave creation or update.

  • Webhook: leave created/edited → push status to CRM
  • Scheduled: every 30 min from 6 AM to 8 PM org time → reconcile, fix drift
  • Nightly: 2 AM → full re-sync for the next 7 days, catches retroactive entries

This three-layer pattern is annoying to build but it’s the only one that survives PTO posted at 11 PM the night before.

The data model in CRM

Don’t dump People’s full leave model into CRM. You only need:

  • User.Currently_Out (boolean)
  • User.Out_Until (date)
  • User.Out_Reason_Code (PTO, sick, training, conference) — coarse, not the full reason
  • User.Last_Attendance_Sync (datetime) — for debugging

That’s it. The full leave record lives in People. CRM gets a summary.

The Deluge sync function

Triggered by webhook and scheduled function alike. Same body. Idempotent.

// sync_user_attendance: callable from webhook or scheduled function
// args: user_email (string), check_date (date, default = today)

void sync_user_attendance(string user_email, date check_date)
{
  if(check_date == null) { check_date = zoho.currentdate; }
  
  // 1. Find the CRM user
  crm_users = zoho.crm.searchRecords("Users", "(email:equals:" + user_email + ")");
  if(crm_users.size() == 0) { return; }
  crm_user_id = crm_users.get(0).get("id");
  
  // 2. Query People for active leave records covering check_date
  // Includes half-day handling: if leave is half-day, currently_out = false
  // unless we want to track that separately. We treat half-day as "in but reduced".
  
  query = Map();
  query.put("emp_email", user_email);
  query.put("date", check_date.toString("yyyy-MM-dd"));
  
  leave_response = zoho.people.getRecords("leave", query);
  
  is_out = false;
  out_until = null;
  reason = null;
  
  for each leave in leave_response.get("leaves")
  {
    leave_type = leave.get("leave_type_code");
    is_half_day = ifnull(leave.get("is_half_day"), false);
    leave_to = toDate(leave.get("to_date"));
    
    if(!is_half_day && leave.get("status") == "approved")
    {
      is_out = true;
      if(out_until == null || leave_to > out_until)
      {
        out_until = leave_to;
        reason = leave_type;
      }
    }
  }
  
  // 3. Idempotent update — only write if anything changed
  current = zoho.crm.getRecordById("Users", crm_user_id);
  current_out = ifnull(current.get("Currently_Out"), false);
  current_until = current.get("Out_Until");
  
  if(current_out != is_out || current_until != out_until)
  {
    zoho.crm.updateRecord("Users", crm_user_id, {
      "Currently_Out": is_out,
      "Out_Until": out_until,
      "Out_Reason_Code": reason,
      "Last_Attendance_Sync": zoho.currenttime
    });
  }
}

Notes:

  • Idempotent. Re-running is safe and cheap.
  • Half-day handled explicitly. Treated as “in” for assignment purposes; create a separate Half_Day_Today field if you need that signal.
  • Last_Attendance_Sync is your debug breadcrumb. If a rep’s status is wrong, check the timestamp first.

The assignment-rule integration

The whole point is leads don’t go to people who are out. So the assignment rule needs to filter on Currently_Out = false before picking a target. Build a “Available AEs” group dynamically: a workflow rule on User updates that maintains group membership.

// On User update, when Currently_Out changes
user_id = input.id;
user = zoho.crm.getRecordById("Users", user_id);

if(user.get("Currently_Out") == true)
{
  // remove from Available_AEs group
  zoho.crm.invokeConnector("crm.users.removeFromGroup", {
    "user_id": user_id,
    "group_name": "Available_AEs"
  });
}
else
{
  zoho.crm.invokeConnector("crm.users.addToGroup", {
    "user_id": user_id,
    "group_name": "Available_AEs"
  });
}

Now your round-robin assignment uses Available_AEs as its pool. Reps who are out, fall out automatically.

The retroactive PTO fix

Reps log time after the fact. Your sync at 6 AM doesn’t know. So at 2 AM nightly, run a sweep of the last 3 days. Find anyone with a new approved leave that overlaps with a date already passed but where their CRM status didn’t reflect it. Don’t try to fix history. Just log it for the manager to review.

// nightly_retro_check
window_start = subDay(zoho.currentdate, 3);
window_end = zoho.currentdate;

people_leaves = zoho.people.getRecords("leave", {
  "from_date": window_start.toString("yyyy-MM-dd"),
  "to_date": window_end.toString("yyyy-MM-dd"),
  "status": "approved",
  "created_after": subDay(zoho.currenttime, 1).toString()
});

retro_count = people_leaves.size();
if(retro_count > 0)
{
  zoho.cliq.postToChannel("revops-alerts", {
    "text": "Retroactive PTO entries in last 24h: " + retro_count.toString() +
            " — check lead assignment for the affected dates."
  });
}

Pitfalls list

  • Don’t sync managers — they don’t need to be marked out for lead assignment; they don’t get leads.
  • Don’t sync ex-employees — filter active users only. Otherwise the sync re-creates ghosts on every run.
  • Don’t dump reason text — privacy. Use coarse codes.
  • Don’t trust webhooks alone — they drop. Reconciliation sweep is the safety net.
  • Don’t sync more than 14 days out — beyond that, plans change. Keep the window tight.

For the broader pattern of cross-product syncing and where it inevitably leaks, see Zoho Campaigns CRM sync gotchas.

Bottom line

Attendance sync is a three-layer problem: webhooks for immediacy, scheduled reconciliation for drift, nightly sweep for retroactive entries. Keep the CRM-side data model minimal — boolean, date, code. Idempotent updates. Build a dynamic “available” group that the assignment rule reads. Don’t try to mirror every detail from People; you’ll spend a quarter doing it and a year fixing it.

[object Object]
Share