Handling Slow External APIs in ServiceNow: A Practical Async Job Pattern

Dinethri ASP
Tera Contributor

If you've integrated with an external API that doesn't respond instantly (a vendor system that has to look something up, process a batch, or generate a report on their end before handing back a result), you've probably run into the limits of a simple synchronous REST call pretty quickly. Here's a pattern for handling those slower, asynchronous-by-nature APIs cleanly, without blocking users or timing out transactions.

 

The problem with treating every API call as synchronous

 

A straightforward integration usually starts as: call the API, wait for the response, use the response. That works fine when the external system answers in a second or two. It falls apart when the external system's own process takes 30 seconds, a few minutes, or longer, because ServiceNow transactions have execution time limits, and a user isn't going to sit on a loading screen for two minutes waiting for a warranty lookup or an inventory check to come back.

 

Some vendor APIs are built around this reality themselves and expose an explicitly asynchronous pattern: you submit a request, get back a job ID immediately, and then poll a separate endpoint (or wait for a callback) to get the actual result once it's ready. If you're integrating with an API like that, trying to force it into a synchronous call-and-wait pattern on the ServiceNow side fights against how the API is designed to be used.

 

The pattern that works

 

Step one: submit and store, don't wait. When a user or process triggers the integration, make the initial API call, get back the job ID or reference token the vendor gives you, and immediately store that reference along with a status of "pending" on your integration tracking record. Return control to the user right away; don't block on the result.

 

Step two: poll asynchronously via a scheduled job. Build a Scheduled Job (or a Flow with a wait/retry pattern, depending on your platform version and preference) that runs on an interval, checks for any tracking records still in "pending" status, and calls the vendor's status/result endpoint for each one. This decouples the original user action entirely from the wait time; the user isn't tied to a transaction that's polling in the background.

 

Step three: handle the terminal states explicitly. Most vendor async APIs have at least three outcomes: complete with a result, still processing, and failed. Don't just handle the happy path. Decide explicitly what happens on failure (retry how many times, notify who, after how long do you give up) as part of the initial design, not as an afterthought once someone reports a stuck record.

 

Step four: set a reasonable timeout ceiling. Pending records that never resolve are inevitable eventually, whether from a vendor outage or a malformed original request. Build in a maximum age after which a still-pending record gets flagged for attention rather than polled forever. Polling indefinitely for a job that's never coming back just adds load without any benefit.

 

Things that bite people building this for the first time

 

Polling too aggressively. It's tempting to poll every few seconds to get results back quickly, but most vendor APIs have rate limits, and hammering their status endpoint can get your integration throttled or flagged. A polling interval that starts modest and backs off the longer a job has been pending (checking every 30 seconds for the first few minutes, then dropping to every few minutes after that) respects the vendor's infrastructure and yours.

 

Losing track of jobs across MID Server or instance restarts. If your tracking relies on anything held in memory rather than stored in a table, a restart mid-poll loses track of in-flight jobs entirely. The tracking record needs to live in the database, not in a script's local state, precisely so a scheduled job can pick up exactly where things left off regardless of what else restarted in between.

 

Not handling duplicate submissions. If a user (or an automated process retrying on a perceived failure) submits the same request twice before the first one resolves, you can end up with two parallel jobs for what should have been one. A simple check for an existing pending record for the same request before submitting a new one prevents this, and it's easy to skip during initial development because it never shows up in early testing.

 

Assuming the vendor's job ID is globally unique forever. Some vendor systems reuse job IDs after some retention period. If your tracking table's lookup logic assumes a job ID is unique across all time rather than unique within a reasonable window, you can eventually get a false match against an old, unrelated job. Scope your lookups by both job ID and a reasonable time window, not job ID alone.

 

Takeaway

The core idea is simple even though the implementation has a few sharp edges: don't force a slow, asynchronous-by-design API into a synchronous call-and-wait model just because that's the simpler mental model to build first. Submit, store a reference, poll in the background, and handle every terminal state explicitly. Most of the actual engineering effort here goes into the edge cases (restarts, duplicates, rate limits, stale IDs) rather than the happy-path flow, which is usually the easy 20% of the work.

 

Curious whether others have moved this kind of polling logic into Flow Designer's wait conditions versus keeping it in a traditional Scheduled Job. I've seen arguments for both and haven't settled on a strong preference myself.

0 REPLIES 0