Back to blog

Guides · Feb 16, 2026

Google Apps Script external scheduler: one dashboard for all your exec URLs

Apps Script time-driven triggers work fine for one script owned by one user. They start to bite once you have ten scripts across three Google accounts, no cross-script dashboard, and no per-IANA timezone field. Here is the external scheduler pattern that drives every /exec URL from one place.
crontap.com / blog
Apps Script time-driven triggers are tied to one Google account, with no cross-script dashboard, no per-IANA timezone field, and a quiet daily-runtime quota. Here is the external scheduler pattern that puts every script.google.com /exec URL on one dashboard with real cron, real timezones, and per-job failure alerts.

You wrote one Apps Script to send the weekly digest, then another to dedupe a sheet, then a third to refresh an OAuth token, then six more. They all live in different Google accounts (yours, the founder's, the one used to own a shared drive), each with its own time-driven triggers tab, each with its own executions log. Once the count crosses 10, the answer to "did the Tuesday script fire" is "I will check, give me a few minutes". That is not a great place to be. Here is the external scheduler pattern that puts every script.google.com/macros/s/.../exec URL on one dashboard with real cron, real timezones, and real failure alerts.

If you just want the short version: deploy each Apps Script as a web app, copy its /exec URL, and point Crontap at it. You get every 1 minute on Pro, per-IANA timezones, one place to see every fire across every Google account, and per-job email / webhook (Slack / Discord / Telegram) alerts when something returns 500 instead of 200.

What Apps Script triggers do today

Apps Script ships with two scheduling primitives, both documented in the triggers guide. The first is event-based: respond to a form submit, a sheet edit, a calendar change. The second is time-driven: run a function on a clock.

Time-driven triggers are the cron-shaped one. You open the script, click the Triggers icon, pick a function, pick a frequency (every minute, every 5 minutes, every hour, daily at a window), and Apps Script fires the function at roughly that cadence. The "roughly" matters: time-driven triggers fire within an hour-long window for daily triggers, a 15-minute window for hourly triggers, and so on, and the windows are not configurable.

The trigger lives inside the script project, owned by the Google account that created it. The executions log lives in the same project, scoped to that account. The quota is per account, applied across every script that account owns, per the Apps Script quotas page.

For one script owned by one person, this is fine. The friction shows up when one of those numbers grows.

The walls teams hit once they have 10 plus scripts

Every team we have watched on Apps Script ends up at the same handful of paper cuts.

Triggers are bound to one Google account

A time-driven trigger is owned by the user who created it. If that user leaves, transferring scripts is a manual ownership-transfer dance per script, and the triggers do not survive intact. We watched an Apps Script user with multiple scripts spend an afternoon migrating triggers off a departing colleague's account; the executions log on the new owner's side started fresh, so the audit trail of what fired before the migration sits in an account they can no longer log into. The fix is to stop letting one account own the schedules. Either give every script its own service account, or move the schedule out of Apps Script entirely.

The executions log is one tab per script

Apps Script's executions log is a powerful debugger when you are inside a script. As an operations dashboard across a team's scripts, it does not work, because there is no cross-script view. To see "did all 12 scripts that should have fired this morning actually fire", you open 12 tabs.

Most teams give up on that view after the third or fourth time. The tab nobody opens turns into the tab nobody trusts, which turns into a quiet "well, we will know if customers complain" failure mode.

The cadence floor is coarser than it looks

Apps Script time-driven triggers nominally support "every minute", but the cadence floor is also a quota floor: minute-level triggers count against your daily script-runtime quota fast, especially if the function touches an external API or a meaningful-sized Sheet. A team running 10+ Apps Scripts at minute-level cadence will quietly hit the trigger total runtime quota before the day is out. The unofficial escape valve, and the one we keep seeing, is to stop using Apps Script as the scheduler and use it only as the runtime.

No timezone field, no DST handling

Time-driven triggers run in the script project's timezone, which is a per-project setting buried in the project settings. There is no per-trigger timezone field, no IANA picker on the trigger UI, and no DST-aware "fire at 08:00 local" expression. If you want the morning digest at 08:00 in Europe/London and 08:00 in America/New_York from the same script, you ship two scripts or two functions and accept the offset math twice a year when DST changes.

Deploy as web app: the exec URL pattern

Apps Script is more than the triggers tab. The relevant capability for an external scheduler is documented in the Web Apps guide: any function can be exposed as an HTTPS endpoint by deploying the script as a web app.

The shape is simple. You add a doGet(e) or doPost(e) function to the script, click Deploy, pick Web app, choose who can run it (you, anyone in the workspace, or anyone with the link), and copy the URL. The URL has the shape https://script.google.com/macros/s/<deployment-id>/exec. Hitting that URL runs the function as the deploying user with that user's authorization, returns whatever the function returns, and writes a row in the executions log just like a triggered run would.

function doPost(e) {
  if (e.parameter.token !== PropertiesService.getScriptProperties().getProperty("CRON_TOKEN")) {
    return ContentService.createTextOutput(JSON.stringify({ ok: false }))
      .setMimeType(ContentService.MimeType.JSON);
  }

  refreshOauthTokens();
  return ContentService.createTextOutput(JSON.stringify({ ok: true }))
    .setMimeType(ContentService.MimeType.JSON);
}

That is the full surface. The function reads a token from script properties, checks it against the request, and runs the work. From the outside, it is a regular HTTPS endpoint that takes a POST.

This is the primitive every external scheduler builds on. Once you have an /exec URL per script, you stop caring about the Apps Script triggers UI, because the cron lives somewhere else.

The external cron pattern, click by click

Here is the shape we recommend. It works for any Apps Script project on any Google account, free or Workspace, and you do not need any extra Google product.

Crontap (cron)  ->  HTTPS POST  ->  https://script.google.com/macros/s/<id>/exec  ->  the actual work

Apps Script still owns the runtime. Crontap owns the clock. The contract between them is one HTTPS POST per cadence with a token your doPost function checks.

Step 1: Add a token to script properties

Open the script, go to Project Settings, scroll to Script Properties, click Add script property. Name it CRON_TOKEN, set the value to a long random string. Generate the value locally with:

openssl rand -base64 32

You only see the value once after saving, so paste it into your password manager before you close the page.

Step 2: Wrap the work in doPost(e)

Add a doPost(e) to the script that checks the token, then calls whatever function the script already has. If the script previously had a time-driven trigger pointing at runDigest, the new shape is:

function doPost(e) {
  const expected = PropertiesService.getScriptProperties().getProperty("CRON_TOKEN");
  if (e.parameter.token !== expected) {
    return ContentService.createTextOutput("unauthorized");
  }

  runDigest();
  return ContentService.createTextOutput("ok");
}

The runDigest function is unchanged. The new doPost is the boundary; everything inside it is your existing code.

Step 3: Deploy as web app

Click Deploy, then New deployment, then pick Web app. Set:

  1. Description. Whatever helps you find this deployment later, e.g. "Cron entry point v1".
  2. Execute as. Pick Me ([email protected]) if the script needs your authorization on Sheets, Drive, Gmail. Pick a service account if you want the deployment decoupled from any one user.
  3. Who has access. Pick Anyone. The token check inside doPost is the actual auth boundary, not the workspace permission.

Click Deploy, copy the /exec URL. The URL looks like https://script.google.com/macros/s/AKfycbx.../exec.

Step 4: Point Crontap at the URL

Head to Crontap and create a new schedule.

  1. URL. Paste the /exec URL plus the token as a query parameter, e.g. https://script.google.com/macros/s/AKfycbx.../exec?token=<your CRON_TOKEN>. The token rides as a query parameter because Apps Script web apps follow redirects in a way that strips custom headers; the token in e.parameter.token is the reliable read path.
  2. Method. POST.
  3. Cadence. Type plain English ("every 5 minutes") or paste a cron expression. Crontap previews the next 5 fires inline so you can sanity-check before saving.
  4. Timezone. Pick the IANA zone the schedule should run in. Apps Script projects have one project timezone; Crontap stores timezone per schedule and handles DST.
  5. Failure alerts. Add an integration: email / webhook (Slack / Discord / Telegram). Crontap fires on 4xx and 5xx with the response body and timing in the payload.

Press Perform test to fire a real request before you trust the cadence. If the script returns ok, you are done. If it returns unauthorized, the token in the URL does not match the script property. If you see a 5xx, the script itself errored and the alerting just proved itself.

Fix this in 60 seconds with Crontap. Free tier available. No credit card. Schedule your first job.

Auth options: URL token vs Google authorization

The pattern above uses a shared token in the URL. There are two other shapes worth knowing.

Token in the URL (recommended)

Easy to set up, easy to rotate, easy to audit. The token is a script property; rotating it is "edit the property, save, update the Crontap URL". The downside is that the token lands in the script's executions log as part of the request URL, so anyone with read access to the log can read it. For most teams, that is the same set of people who already have the script open.

Google account authorization (Execute as a specific user)

Apps Script web apps can be deployed as "Execute as: Me", which means the function runs with that user's OAuth scopes. If your script needs to write to [email protected]'s personal Drive folder, you deploy as Me with that account. The downside is that Apps Script bound to one account is the original problem; if that account leaves, the deployment authorization needs migrating.

We have seen teams keep their per-user-scoped scripts on Execute-as-me and use a token only inside doPost for cron auth, while moving anything that does not need a specific user's identity to a service account or a Workspace-shared account. That hybrid is the most common shape.

Worked example: 10 plus scripts, one dashboard

Take an Apps Script user with multiple scripts: 12 scripts owned across 3 Google accounts. The cadence mix is something like this:

  • 4 scripts firing every 5 minutes (sheet sync, OAuth refresh, two webhook polls).
  • 5 scripts firing daily at named local times (07:00 Europe/London digest, 09:00 America/New_York digest, 18:00 Asia/Singapore recap, 02:00 UTC cleanup, 05:00 UTC backup).
  • 3 scripts firing weekly Monday 06:00 Europe/London for the week-ahead reports.

In the Apps Script triggers UI, that is 12 tabs of trigger config across 3 logged-in browser sessions, one project timezone per script, no central view of "did all 12 fire this morning", and no failure alert routing without writing custom error handlers per script. Cadence changes are a click into the script, into the triggers tab, into the trigger row, save, repeat.

After the move, every script is deployed as a web app and the 12 /exec URLs become 12 schedules in one Crontap dashboard, each with its own IANA zone, its own cadence, its own Slack alert. Cadence changes are a dropdown. Adding a 13th script is one new schedule. The script code and the runtime are unchanged; only the clock moved.

When to keep using Apps Script time-driven triggers

External cron is a shape, not a religion. The built-in is the right answer when the script is owned by one user and the ownership is stable, the cadence is daily or coarser and an hour-long fire window is fine, the script lives by itself without a fleet of siblings that need a cross-script view, or you are inside a tightly-governed Workspace where ownership and quotas are not a concern.

For everything else, especially as the count of scripts grows, the external pattern reads cleaner.

FAQ

Will Crontap work with Apps Script on a free Google account?

Yes. Web app deployments work the same on free Google accounts and Workspace. The /exec URL is publicly reachable as long as you set Who has access: Anyone at deploy time. The token check in doPost is the auth boundary.

Does the executions log still show every run?

Yes. Every POST Crontap fires runs through doPost, which means it shows up in the script's executions log with the function name, runtime, and any errors thrown. You keep the Apps Script-side observability you already have, plus you gain Crontap's own request and response history alongside.

What about the Apps Script daily runtime quota?

The daily script runtime quota documented on the quotas page still applies. Crontap firing the script every minute does not give the script extra runtime; it just gives you a real cron. If the work is too heavy for Apps Script's quota, the answer is the same as before: move the heavy work to a backend that does not have the cap, and let Apps Script be a thin trigger.

Can I rotate the token without breaking the schedule?

Yes. Update the CRON_TOKEN script property to the new value, then update the Crontap schedule's URL (or its query parameter) to match. The two updates can happen in either order; Crontap stores the URL encrypted and the next fire reads the new value.

Can I keep one Apps Script trigger and add Crontap for the rest?

Yes, they run independently. Many teams keep the simplest one or two triggers in Apps Script and add Crontap for everything that needs sub-hour cadence, per-IANA timezones, or cross-script dashboards. The two do not conflict.

What if my script needs to run as a specific user?

Deploy as web app with Execute as: Me under the user that owns the relevant scopes (Gmail, Drive, Calendar). The token check in doPost still runs first, so an unauthenticated POST is rejected before any user context is touched. The downside is the same as before: that user becomes a single point of ownership, so plan the rotation when the user leaves.

References

Related on Crontap

From the blog

Read the blog

Guides, patterns and product updates.

Tutorials on scheduling API calls, webhooks and automations, plus deep dives into cron syntax, timezones and reliability.

Alternatives

Vercel Cron every minute: beating the Hobby hourly limit

Vercel Cron caps Hobby at hourly cadence and 5 jobs, and ties every change to a redeploy. Here is the external cron pattern teams use to ship per-minute schedules, per-IANA timezones, and one dashboard across projects without paying $20/mo per user for Pro.

Alternatives

Cloud Run cron without Cloud Scheduler

Cloud Scheduler costs $0.10 per job per month after the first 3 and asks for OIDC plus IAM bindings on every target. Here is the IAM-free pattern Cloud Run teams use to fire their .run.app URLs on a clock with one bearer token and one dashboard across every GCP project.

Alternatives

Heroku Scheduler alternative: any cron expression without the add-on

Heroku Scheduler caps you at three cadences and account-wide UTC, and spins a one-off dyno per run. Here is the external cron pattern that gives you any cron expression, per-schedule timezones, and zero per-execution dyno spin-up cost.

Guides

Running an OpenAI sentiment pipeline on a real scheduler

OpenAI batch work needs a clock, not a user session. Here is the scheduled HTTP-route pattern teams use to drain LLM batches at a sustainable rate inside OpenAI's rate limits, with per-task failure alerts.

Reference

Cron syntax cheat sheet with real-world examples

Cron syntax without the math. Every pattern you're likely to reach for (every 5 minutes, weekdays, business hours, first of the month), with a practical example and a link to a free debugger.