When Clockify Changed Its API and Our Invoice Workflow Went Quiet for Five Days

When Clockify Changed Its API and Our Invoice Workflow Went Quiet for Five Days

The last successful Slack report landed March 29. I didn’t notice until April 3.

That’s the thing about automation that works quietly. You stop watching it. And when it breaks quietly too, the gap can stretch into days before anyone asks “wait, where’s the report?”

So here’s what happened, why it happened, and what we changed so it doesn’t happen the same way again.

What the Workflow Does (and Why It Exists)

Every 1st and 16th of the month, an n8n workflow fires at 9:00 AM. It pulls time entries from Clockify’s Reports API for our seven-person team, runs them through three validation rules, calculates per-member costs, aggregates client invoice totals, and posts a summary to Slack with approval buttons.

The whole thing exists because I don’t want to manually review 464 time entries every two weeks. I want the workflow to surface the 12 that look wrong, show me the client invoice summary, and let me approve or flag for review in one click.

When it works, it takes me about 90 seconds to process a bi-monthly report. When it breaks silently (which is what happened in March), I lose visibility entirely.

n8n Clockify invoice automation workflow diagram

Clockify Cost Report & Invoice Agent. The Generate Report node (orange) is where Clockify’s changed response format enters. The Calculate Costs node (red) is where parseDuration() crashed.

The Error

On April 3, I opened the failed execution in n8n. It had processed 464 entries, then died mid-calculation:

TypeError: isoDuration.match is not a function

The stack traced to parseDuration(), a helper that converts Clockify’s time interval duration field into milliseconds so we can do math on it.

Here’s the function as it was written:

function parseDuration(isoDuration) {
  if (!isoDuration) return 0;
  const regex = /PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/;
  const matches = isoDuration.match(regex);  // breaks when isoDuration is a number
  if (!matches) return 0;
  const hours = parseInt(matches[1] || 0);
  const minutes = parseInt(matches[2] || 0);
  const seconds = parseInt(matches[3] || 0);
  return (hours * 60 * 60 * 1000) + (minutes * 60 * 1000) + (seconds * 1000);
}

The function assumed isoDuration would be a string, something like "PT38M" (ISO 8601 for 38 minutes). It calls .match() on that string. But .match() is a string method. If isoDuration is a number, calling .match() on it throws a TypeError.

And that’s exactly what Clockify started sending.

What Clockify Changed

At some point in March 2026, Clockify’s Reports API changed the format of timeInterval.duration. Previously it returned ISO 8601 strings:

{
  "timeInterval": {
    "start": "2026-03-15T09:00:00Z",
    "end": "2026-03-15T09:38:00Z",
    "duration": "PT38M"
  }
}

After the change, it started returning integer seconds:

{
  "timeInterval": {
    "start": "2026-03-15T09:00:00Z",
    "end": "2026-03-15T09:38:00Z",
    "duration": 2280
  }
}

2280 is 38 × 60, mathematically equivalent. But (2280).match(regex) throws a TypeError, because numbers don’t have .match().

The change appeared in the actual response payload, the kind of thing you only discover by looking at a raw API response after something breaks.

The Fix

One line, added as the first type guard in parseDuration():

function parseDuration(isoDuration) {
  if (!isoDuration) return 0;
  // Clockify changed timeInterval.duration from ISO 8601 string to integer seconds (March 2026)
  if (typeof isoDuration === 'number') return isoDuration * 1000;
  const regex = /PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/;
  const matches = isoDuration.match(regex);
  if (!matches) return 0;
  const hours = parseInt(matches[1] || 0);
  const minutes = parseInt(matches[2] || 0);
  const seconds = parseInt(matches[3] || 0);
  return (hours * 60 * 60 * 1000) + (minutes * 60 * 1000) + (seconds * 1000);
}

If the input is already a number (seconds), multiply by 1000 and return milliseconds. If it’s a string, parse the ISO 8601 format as before. The rest of the workflow doesn’t change: it still receives milliseconds either way.

✗ Before: breaks on integer input

function parseDuration(isoDuration) {
  if (!isoDuration) return 0;
  const regex =
    /PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/;
  const matches =
    isoDuration.match(regex); // 💥 TypeError
  if (!matches) return 0;
  const hours   = parseInt(matches[1] || 0);
  const minutes = parseInt(matches[2] || 0);
  const seconds = parseInt(matches[3] || 0);
  return (hours * 3600000)
       + (minutes * 60000)
       + (seconds * 1000);
}

✓ After: handles both string and integer
function parseDuration(isoDuration) {
  if (!isoDuration) return 0;
  // Clockify changed timeInterval.duration
  // from ISO 8601 string to integer seconds
  // (March 2026), handle both formats
  if (typeof isoDuration === 'number')
    return isoDuration * 1000;
  const regex =
    /PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/;
  const matches = isoDuration.match(regex);
  if (!matches) return 0;
  const hours   = parseInt(matches[1] || 0);
  const minutes = parseInt(matches[2] || 0);
  const seconds = parseInt(matches[3] || 0);
  return (hours * 3600000)
       + (minutes * 60000)
       + (seconds * 1000);
}

One type guard converts integer seconds to milliseconds before the ISO 8601 parser runs. All downstream nodes still receive milliseconds. No other changes required.

We deployed this fix on April 3, the same day we found the error. The April 16 run processed cleanly.

Why It Took Five Days to Notice

Here’s the part I find more interesting than the bug itself: the detection gap.

The workflow broke on March 29 (the 16th-of-month run). I didn’t notice until April 3. That’s five days of no invoicing data, no Slack report, no visibility into team time entries. Nobody flagged it.

Three things compounded to create that gap.

First: no failure alert on the workflow. The n8n workflow had no error handler node. When it failed, it failed silently. No notification, no Paperclip issue, no email. The workflow just stopped. If you weren’t actively watching the execution history, you wouldn’t know.

Second: both email monitors were inactive. We have two n8n monitoring workflows that watch for system errors and bounce notifications. Both were in deactivated state. I don’t know exactly when they got deactivated (a maintenance window or an accidental toggle), but the secondary notification channel was also dark.

Third: Google OAuth had expired. The Slack node was using an expired Google OAuth credential. So even if an error notification had fired through a different path, Slack delivery would have failed too.

Three independent failure modes. None catastrophic on their own. Together, they created a five-day silence.

Clockify API break detection gap timeline

Five days from silent break to discovery. Any one of the three monitoring gaps (error handler, email monitors, OAuth) would have cut that window to under an hour.

What We Changed About Monitoring

The Clockify API change is a one-time fix. The monitoring gap is the actual problem worth solving.

Here’s what we changed:

Error handler workflows on every scheduled automation. N8n lets you attach a separate error-trigger workflow under workflow settings. It fires on any execution failure and gives you the error message, the execution ID, and a link back to the failed run. We route those to a dedicated #automation-alerts channel in Slack. It’s not elegant: it’s a raw JSON dump, but it means we know within minutes, not days.

Credential expiry monitoring. OAuth tokens expire. API keys get rotated. We added a Cloudron-hosted cron that pings each active n8n credential’s health endpoint weekly and posts to Slack if anything looks stale.

Scheduled workflow staleness check. Every Monday morning, a separate n8n workflow checks the last execution timestamp for each critical automation. If a scheduled workflow hasn’t run within three days of its expected window, it flags it. The Clockify workflow would have been caught Monday March 31, not April 3.

None of these are sophisticated. But the Clockify break would have been detected in under an hour with any one of them in place.

The Broader Problem with External API Dependencies

There’s a real tension in automation work between trusting external APIs and building defensively against them.

The easy framing is “always validate your inputs.” But parseDuration() was already validating: it had a null check, it checked for regex match failure. What it didn’t do was check the type of its input, because Clockify’s documentation said the field would always be a string.

When you’re integrating with an external API, you’re trusting the other party’s contract. And API contracts break. Sometimes with deprecation notices. Sometimes with changelogs. Sometimes without any warning at all.

Right? Because the moment you assume a field will always be a certain type, you’ve built a dependency on undocumented behavior. Clockify can change their response format. We can’t prevent that. What we can control is how fast we detect the break and how much business logic sits downstream of a brittle assumption.

In this case: 464 entries, one changed field type, the entire calculation chain fails. parseDuration() appeared in three separate Code nodes: Validate Entries, Calculate Costs, and Aggregate Invoices. All three stopped working simultaneously because they all made the same assumption about the same field.

The smarter architecture is a single normalization step at the API boundary, before any business logic touches the data. One place where the raw Clockify response gets cleaned and typed. Three downstream nodes that never see raw API fields directly. This boundary-isolation approach, along with schema validation, exponential backoff, and circuit breakers, is what we cover in API Integration Patterns for Business Automation.

What I’d add to any function that calls string methods on external data:

// Defensive type check before calling string methods on API data
if (typeof value !== 'string') {
  throw new Error(`Expected string for field "duration", got ${typeof value}: ${JSON.stringify(value)}`);
  // Or handle the non-string case explicitly, as we did above
}

It’s a two-second addition that makes failures explicit and traceable instead of cryptic.

If You’re Using Clockify’s Reports API

A few practical things worth knowing:

Add the type guard to parseDuration now. Even if you’re still getting ISO 8601 strings, you’ll be protected when the format change reaches your account or region.

Don’t duplicate parseDuration across multiple Code nodes. Extract it to a sub-workflow or normalize the Clockify response in your first processing node before the data branches. One type assumption in one place. (n8n’s Code node docs explain sub-workflow patterns if you need a starting point.)

Set up n8n’s Error Workflow. It’s under workflow settings → Error Workflow. Takes five minutes. Routes any execution failure to a workflow of your choice: route that to Slack or create a tracking issue in your project management tool.

Include integer-format samples in your test data. If you’ve built test fixtures with Clockify API responses, add "duration": 2280 alongside "duration": "PT38M". Your tests should cover both formats now.


This is the kind of incident that’s embarrassing to write about: a five-day gap, a one-line fix, three compounding monitoring failures. But the Clockify API change is worth documenting because it’s silent, and if you’re running any automation against their Reports API that touches timeInterval.duration, you’re going to hit it.

If your automation stack depends on Clockify, Xero, or any other API that has ever updated without notice, a 30-minute review of your error handling setup is worth more than a day of debugging after a silent break. Get in touch: we review n8n workflows for small businesses and internal teams.

Vlad Tudorie is the founder of Serenichron, an AI automation consultancy based in Bucharest, Romania. The Clockify Cost Report & Invoice Agent described here runs on n8n self-hosted via Cloudron on the Serenichron infrastructure stack.

Enjoyed This Article? Let’s Talk.

No pressure, no confusing tech talk—just clear advice to help you move forward.

Book Your Free 30-Minute Call