# Advanced Scheduler > Advanced Scheduler is a Heroku add-on that provides task scheduling as a service. It executes recurring and one-off tasks as Heroku one-off dynos — recurring schedules are defined with standard cron expressions (or a schedule helper), one-off schedules with a specific date and time. It is language-agnostic and works with any language supported by Heroku. It is built on the same principles as **Heroku Scheduler** but is a separate product. Compared to Heroku Scheduler it adds selectable dyno types, per-trigger timeouts (up to 86400 seconds), execution history, failure monitoring and email alerts, real-time execution logs, a Service API, and a Heroku CLI plugin. Existing Heroku Scheduler jobs can be imported and behave identically after import. A scheduled unit of work is called a **trigger**. Because tasks run as one-off dynos, they count toward the app's dyno usage and its concurrent one-off dyno limit. This is `llms-full.txt` — the full-text companion to `llms.txt`. Every section below is the complete content of the page it comes from, not just a link, so a consumer of this file doesn't need to fetch anything further for that content. Sections that are intentionally link-only (third-party/generic Heroku platform docs, or genuinely live/dynamic pages like the status page) are marked as such and kept as a link, matching `llms.txt`. Each section is prefaced with an HTML comment naming its source URL. --- ## Getting Started ### Provisioning the add-on Advanced Scheduler can be attached to a Heroku application via the Heroku CLI: ``` $ heroku addons:create advanced-scheduler -a sharp-mountain-4005 -----> Adding advanced-scheduler to sharp-mountain-4005... done, v18 (free) ``` A list of all plans available can be found at elements.heroku.com/addons/advanced-scheduler. ### Quick Start (5 minutes) This guide walks you through installing Advanced Scheduler, defining a task, running it once, and scheduling it — all in about 5 minutes. #### 1. Install the add-on Attach Advanced Scheduler to your Heroku application using the CLI: ``` $ heroku addons:create advanced-scheduler -a sharp-mountain-4005 ``` #### 2. Define a task A task is any command that can be executed inside your application. This can be a framework-specific task (for example a rake task), a script in `bin/`, or a process type defined in your `Procfile`. Make sure your task: - Runs to completion without user interaction and does not run indefinitely - Exits with status code `0` on success #### 3. Test the task on Heroku Before scheduling your task, verify that it runs correctly on Heroku by executing it manually: ``` $ heroku run ``` Example: ``` $ heroku run node send-reminders.js ``` #### 4. Schedule the task Open the Advanced Scheduler dashboard: ``` $ heroku addons:open advanced-scheduler -a sharp-mountain-4005 ``` Create a new trigger and configure: 1. The command to execute (or a Procfile process type) 2. Whether the task should run once at a specific date and time or on a recurring schedule 3. (Optional) Timeout and dyno type New triggers are active by default. #### 5. Verify execution and logs When the trigger runs, the task executes in a one-off dyno and writes logs to your application logs. To view logs in real time using the CLI: ``` $ heroku logs -t -d advanced-scheduler -a sharp-mountain-4005 ``` #### Next steps - Monitor task executions and failures: Task monitoring - Run longer jobs safely: Long-running tasks - Import existing jobs from Heroku Scheduler: Import Heroku Scheduler Jobs - Automate trigger management via API: Service API - Create and manage triggers via CLI: Heroku CLI Plugin ### Defining tasks Tasks are any command that can be run in your application. Advanced Scheduler is language-agnostic, meaning it works with any language or framework supported on Heroku. Regardless of the language or framework you use, make sure your task: - Runs to completion without user interaction - Does not run indefinitely - Exits with status code `0` on success and a non-zero code on failure #### Node.js Create a script at `bin/send-reminders.js`: ```javascript #!/usr/bin/env node 'use strict'; const db = require('../lib/db'); let connected = false; let isShuttingDown = false; async function shutdown(exitCode) { if (isShuttingDown) return; isShuttingDown = true; if (connected) { try { await db.close(); } catch (err) { console.warn('Warning: db.close() failed during shutdown:', err.message); } } process.exitCode = exitCode; } process.on('uncaughtException', (err) => { console.error('Uncaught exception — exiting immediately:', err); process.exit(1); }); process.on('unhandledRejection', (reason) => { console.error('Unhandled rejection:', reason); shutdown(1); }); (async () => { try { console.log('Sending reminders...'); await db.connect(); connected = true; await db.sendReminders(); console.log('done.'); await shutdown(0); } catch (err) { console.error('Task failed:', err.message); await shutdown(1); } })(); ``` Schedule this task using the command: ``` node bin/send-reminders.js ``` Alternatively, define the task as a script in your `package.json`: ```json { "scripts": { "send-reminders": "node bin/send-reminders.js" } } ``` Schedule using: ``` npm run send-reminders ``` Close all open resources and let Node exit naturally. Prefer `process.exitCode` over `process.exit()` in normal control flow — `process.exit()` forcibly terminates the event loop before pending I/O or logs finish flushing. The exception is inside `uncaughtException` handlers, where the process is already in an unsafe state and must be exited explicitly with `process.exit()`. #### Python **Standalone script** Create a script at `scripts/send_reminders.py`: ```python #!/usr/bin/env python3 import sys def main() -> int: try: print('Sending reminders...') # your task logic here print('done.') return 0 except Exception as e: print(f'Task failed: {e}', file=sys.stderr) return 1 if __name__ == '__main__': raise SystemExit(main()) ``` Schedule this task using the command: ``` python scripts/send_reminders.py ``` **Django management command** For Django applications, create the file `myapp/management/commands/send_reminders.py`: ```python from django.core.management.base import BaseCommand, CommandError from myapp.mailer import send_reminders class Command(BaseCommand): help = 'Send reminders to users' def handle(self, *args, **options): try: self.stdout.write('Sending reminders...') send_reminders() self.stdout.write('done.') except Exception as e: raise CommandError(f'Task failed: {e}') from e ``` Schedule this task using the command: ``` python manage.py send_reminders ``` **Flask CLI command** For Flask applications, register a custom CLI command inside your application factory: ```python # app/__init__.py import sys import click from flask import Flask def create_app(): app = Flask(__name__) @app.cli.command('send-reminders') def send_reminders_command(): """Send reminders to users.""" try: click.echo('Sending reminders...') # your task logic here click.echo('done.') except Exception as e: click.echo(f'Task failed: {e}', err=True) sys.exit(1) return app ``` Schedule this task using the command: ``` flask --app app:create_app send-reminders ``` #### PHP **Standalone script** Create a script at `scripts/send-reminders.php`: ```php getMessage()}\n"); $exitCode = 1; } exit($exitCode); ``` Schedule this task using the command: ``` php scripts/send-reminders.php ``` **Laravel Artisan command** For Laravel applications, create a custom Artisan command. Generate the command using: ``` $ php artisan make:command SendReminders ``` This creates `app/Console/Commands/SendReminders.php`. Update it to fit your needs: ```php info('Sending reminders...'); $mailer->sendReminders(); $this->info('done.'); return Command::SUCCESS; } catch (\Throwable $e) { $this->error("Task failed: {$e->getMessage()}"); return Command::FAILURE; } } } ``` Schedule this task using the command: ``` php artisan reminders:send ``` **Symfony console command** For Symfony applications, create a console command in `src/Command/SendRemindersCommand.php`: ```php writeln('Sending reminders...'); // task logic here $output->writeln('done.'); return Command::SUCCESS; } catch (\Throwable $e) { $output->writeln("Task failed: {$e->getMessage()}"); return Command::FAILURE; } } } ``` Schedule this task using the command: ``` php bin/console app:send-reminders ``` #### Ruby **Rake task** For Rails applications, copy the code below into `lib/tasks/scheduler.rake`: ```ruby desc "This task is called by the Advanced Scheduler add-on" task :send_reminders => :environment do puts "Sending reminders..." User.send_reminders puts "done." end ``` Schedule this task using the command: ``` bundle exec rake send_reminders ``` **Standalone script** An example `bin/send-reminders` script: ```ruby #!/usr/bin/env ruby require_relative '../lib/mailer' begin puts 'Sending reminders...' Mailer.send_reminders puts 'done.' exit 0 rescue => e $stderr.puts "Task failed: #{e.message}" $stderr.puts e.backtrace.join("\n") exit 1 end ``` #### Java **Spring Boot ApplicationRunner** For Spring Boot applications, use an `ApplicationRunner` to execute your task and then shut down: ```java package com.example.task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SendRemindersApplication implements ApplicationRunner { private static final Logger logger = LoggerFactory.getLogger(SendRemindersApplication.class); private final ReminderService reminderService; public SendRemindersApplication(ReminderService reminderService) { this.reminderService = reminderService; } public static void main(String[] args) { System.exit( SpringApplication.exit( SpringApplication.run(SendRemindersApplication.class, args) ) ); } @Override public void run(ApplicationArguments args) { logger.info("Sending reminders..."); reminderService.sendReminders(); logger.info("done."); } } ``` Build the JAR and schedule this task using the command: ``` java -jar target/send-reminders.jar ``` Always wrap `SpringApplication.run()` with `SpringApplication.exit()` and pass the result to `System.exit()`. Without this, background threads in the Spring context will keep the JVM — and your one-off dyno — running indefinitely. **Executable JAR with a main class** For non-Spring applications, create a class with a `main` method: ```java package com.example; public class SendReminders { public static void main(String[] args) { try { System.out.println("Sending reminders..."); // task logic here System.out.println("done."); System.exit(0); } catch (Exception e) { System.err.println("Task failed: " + e.getMessage()); e.printStackTrace(System.err); System.exit(1); } } } ``` Schedule this task using the command: ``` java -cp target/myapp.jar com.example.SendReminders ``` #### Go In Go, compile your task into a binary by placing it under `cmd/` in your repository. Reference the compiled binary in your `Procfile` so Heroku knows how to run it. Create a task at `cmd/send-reminders/main.go`: ```go package main import ( "context" "fmt" "log" "os" "os/signal" "syscall" "github.com/example/myapp/internal/mailer" ) func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop() if err := run(ctx); err != nil { log.Printf("ERROR: %v", err) os.Exit(1) } } func run(ctx context.Context) error { fmt.Println("Sending reminders...") if err := mailer.SendReminders(ctx); err != nil { return fmt.Errorf("send reminders: %w", err) } fmt.Println("done.") return nil } ``` Once built, reference the binary in your `Procfile`. The output path depends on your build setup: ``` send-reminders: ``` Schedule this task using the process type name: ``` send-reminders ``` #### Scala In Scala, use sbt-assembly to build a fat JAR and define a main object for your task. Add the plugin to `project/plugins.sbt`: ``` addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.2.0") ``` Set a stable jar name in `build.sbt`: ``` assembly / assemblyJarName := "myapp-assembly.jar" ``` Create a task object at `src/main/scala/com/example/tasks/SendReminders.scala`: ```scala package com.example.tasks object SendReminders { def main(args: Array[String]): Unit = { try { println("Sending reminders...") Mailer.sendReminders() println("done.") sys.exit(0) } catch { case e: Exception => System.err.println(s"Task failed: ${e.getMessage}") e.printStackTrace(System.err) sys.exit(1) } } } ``` Build the JAR with `sbt assembly`, then define the process type in your `Procfile`: ``` send-reminders: java -cp target/scala-2.13/myapp-assembly.jar com.example.tasks.SendReminders ``` Schedule this task using the process type name: ``` send-reminders ``` #### Clojure In Clojure, use Leiningen to define your task as a namespace with a `-main` function and run it from an uberjar. Create a task namespace at `src/myapp/tasks/send_reminders.clj`: ```clojure (ns myapp.tasks.send-reminders (:require [myapp.mailer :as mailer]) (:gen-class)) (defn -main [& _args] (try (println "Sending reminders...") (mailer/send-reminders!) (println "done.") (System/exit 0) (catch Exception e (binding [*out* *err*] (println (str "Task failed: " (.getMessage e)))) (System/exit 1)))) ``` Build the uberjar and schedule this task using the command: ``` java -cp target/myapp-standalone.jar clojure.main -m myapp.tasks.send-reminders ``` You can also define an alias in `project.clj`: ``` :aliases {"send-reminders" ["run" "-m" "myapp.tasks.send-reminders"]} ``` Then schedule using: ``` lein send-reminders ``` Always call `(System/exit 0)` or `(System/exit 1)` explicitly in Clojure tasks. Without it, background threads in the Clojure runtime will keep the JVM — and your one-off dyno — running indefinitely. ### Testing tasks Once you have written your task and see that it is functioning locally, the next step is to deploy your application and test your task on Heroku. To do so, use `heroku run` to run your task on Heroku: ``` $ heroku run ``` --- ## Scheduling ### Scheduling tasks To schedule a task, you need to create a new active trigger for it. Triggers are configured using the Advanced Scheduler dashboard. Enter the task and specify if the trigger needs to be executed only once or at a certain time interval. For one-off triggers, define a date and time at which the task should be executed. For recurring triggers, provide a standard cron expression or use the schedule helper to define the interval. You can verify cron expressions at crontab.guru. By default, a new trigger will be activated directly after creation. If you do not want this behaviour, you can configure the trigger to stay inactive. Instead of specifying a command, you can specify one of the process types in your app's Procfile. The command associated with the process type will then be executed, together with any parameters you supply. Note that for each plan there are limits on the total number of tasks that can be scheduled at one point in time, as well as the total number of task executions in one month. Once one of these limits is exceeded, attempts to schedule a new task will fail. ### Dyno Type When configuring a trigger, select a dyno type available within the dyno tier configured for your Heroku app. In other words, you can only use for example `standard-2x` for your one-off dynos when you use the Professional tier for your formation dynos. Heroku offers the following dyno tiers: - **Eco tier**: An app that uses `eco` dynos can only use `eco` dynos for its one-off dynos. - **Basic tier**: An app that uses `basic` dynos can only use `basic` dynos for its one-off dynos. - **Professional tier**: An app that uses Professional-tier dynos (`standard` and `performance`) can use `standard-1x`, `standard-2x`, `performance-m`, `performance-l`, `performance-l-ram`, `performance-xl` and `performance-2xl` for its one-off dynos. - **Private tier**: An app that uses Private-tier dynos can use `private-s`, `private-m`, `private-l`, `private-l-ram`, `private-xl` and `private-2xl` for its one-off dynos. - **Shield tier**: An app that uses Shield-tier dynos can use `shield-s`, `shield-m`, `shield-l`, `shield-l-ram`, `shield-xl` and `shield-2xl` for its one-off dynos. When a trigger is configured to use a type of dyno that is not available for your Heroku app, Advanced Scheduler will automatically fall back to Heroku's default dyno type for your app. ### Long-running tasks Although it is generally recommended to keep tasks lightweight and quick to execute, Advanced Scheduler can be used for longer-running tasks. Make sure to only have 1 task running by setting the execution interval higher than the task's maximum execution time. In the event that a specific task does run longer than intended, you can force the task to exit by setting its trigger's timeout value to the maximum allowed execution time. Note that a task can run for up to 24 hours by setting its trigger's timeout value to 86400 seconds. ### Tasks timing out When a task runs longer than its trigger's timeout value, it will be forced to exit shortly after. This mechanism is intended to either prevent a backlog of one-off dynos or to ensure only 1 task of a specific trigger is running at a time. Always aim to make tasks finish by themselves. When a task does time out, make sure to figure out why and take action to prevent it from happening again. Advanced Scheduler supports getting alert notifications on task timeout. Opt-in by reaching out to support@advancedscheduler.io. Note that triggers created in the dashboard default to a timeout of 1800 seconds (30 minutes), while triggers created through the Service API without an explicit `timeout` default to 86400 seconds (24 hours). The maximum timeout value is 86400 seconds or 24 hours; the minimum is 60 seconds (240 seconds for Private, Shield, and `*-Classic`, `*-General`, `*-Compute`, and `*-Memory` dyno sizes). --- ## Dashboard The Advanced Scheduler dashboard allows you to configure one-off and recurring triggers that execute different tasks. You can access the dashboard via the CLI: ``` $ heroku addons:open advanced-scheduler Opening advanced-scheduler for sharp-mountain-4005 ``` or by visiting the Heroku Dashboard and selecting the application in question. Select Advanced Scheduler from the Add-ons menu. --- ## API ### Service API The Advanced Scheduler Service API lets you programmatically automate, extend and combine Advanced Scheduler with other services. You can use the Service API to create and manage triggers. The current version of the Service API is version 2. The reference below documents every endpoint. For a hands-on way to explore and test the Service API, use the Advanced Scheduler Service API Postman collection at documentation.advancedscheduler.io (link only — that page is rendered client-side and isn't reproducible as static text; the full reference below is the text equivalent, extracted and verified against that same Postman collection's underlying data). To interact with the Service API, you will need an API token. This API token can be generated in the Advanced Scheduler dashboard. Note that when generating an API token in the Advanced Scheduler dashboard, the `ADVANCED_SCHEDULER_API_TOKEN` config var will be set on your Heroku application and cause it to restart. ### Service API Reference #### Authentication Bearer authentication is used to interact with the Service API. Construct the `Authorization` header using your API token, prefixed with `Bearer `. ```term $ curl https://api.advancedscheduler.io/triggers \ -H "Authorization: Bearer " ``` #### Response Codes and Errors Every response is a JSON object with a `message` and a `code`: ```json { "message": "OK.", "code": 200 } ``` Successful requests also include the requested resource, under a `trigger`, `triggers`, `execution` or `executions` key, alongside `message` and `code`. The following response codes can be returned by any endpoint: Name | Type | Description | Example -----|------|-------------|-------- **code** | _integer_ | HTTP status code of the response | `200` **message** | _string_ | short, human-readable summary of the response | `"OK."` **errors** | _array_ | present only on `422` responses; one entry per failed validation | see below `422 Unprocessable Entity` responses include an `errors` array with one entry per failed validation: ```json { "message": "Error during validation.", "code": 422, "errors": [ { "location": "body", "parameter": "name", "message": "is required" } ] } ``` Each entry also carries `value` — the rejected input — when the parameter was supplied. | Code | Meaning | | --- | --- | | `200` | The request succeeded. | | `400` | The request body isn't valid JSON, or Heroku rejected the one-off dyno request — for example an unsupported or unavailable dyno size, or a reached one-off dyno limit. | | `401` | The `Authorization` header is missing. | | `403` | The API token is invalid, or the token's plan doesn't include Service API access. | | `404` | The trigger or execution doesn't exist. | | `409` | The requested execution can't be run on this app's current plan; on-demand executions require a paid plan. | | `422` | The request body or parameters failed validation; see `errors`. | | `429` | A plan limit was exceeded, for example the trigger schedule limit or execution count limit. | | `500` | An unexpected error occurred in the service. If it persists, contact support. | | `503` | The service is temporarily unavailable — check the Heroku status page at status.heroku.com. | A `500` or `503` response may not follow the `message`/`code` shape shown above — it can originate from Heroku's routing layer before the request reaches the Service API, in which case the body won't be JSON. #### Trigger A scheduled unit of work. Because tasks run as one-off dynos, they count toward the app's dyno usage and its concurrent one-off dyno limit. Name | Type | Description | Example -----|------|-------------|-------- **uuid** | _uuid_ | unique identifier of trigger | `"01234567-89ab-cdef-0123-456789abcdef"` **name** | _string_ | name of trigger **min length:** `1` **max length:** `150` | `"Monday morning newsletter"` **resourceUUID** | _uuid_ | identifier of the Heroku add-on resource that owns this trigger; inferred from the API token, read-only via the Service API | `"01234567-89ab-cdef-0123-456789abcdef"` **frequencyType** | _string_ | whether the trigger runs once at a specific date and time, or repeatedly on a schedule **one of:** `one-off` or `recurring` | `"recurring"` **schedule** | _string_ | a standard 5-field cron expression (minimum granularity 1 minute), for `recurring` triggers, or a date and time formatted as `YYYY-MM-DD hh:mm:ss` in the trigger's `timezone`, for `one-off` triggers **note:** for `one-off` triggers the time must be in the future and its seconds component must be `00` | `"0 0 * * *"` **value** | _string_ | command executed inside the one-off dyno, or a Procfile process type — see Defining tasks **min length:** `1` **max length:** `1000` | `"npm run send-reminders"` **timezone** | _string_ | IANA time zone name used to interpret `schedule` | `"America/New_York"` **state** | _string_ | **one of:** `active` or `inactive` when creating or updating a trigger; a `one-off` trigger's state becomes `completed` after it runs | `"active"` **dyno** | _string_ | size of the one-off dyno used to execute the task, constrained by your app's dyno tier — see Dyno Type **one of:** `Eco`, `Basic`, `Standard-1X`, `Standard-2X`, `Performance-M`, `Performance-L`, `Performance-L-RAM`, `Performance-XL`, `Performance-2XL`, `Private-S`, `Private-M`, `Private-L`, `Private-L-RAM`, `Private-XL`, `Private-2XL`, `Shield-S`, `Shield-M`, `Shield-L`, `Shield-L-RAM`, `Shield-XL`, `Shield-2XL`, `1X-Classic`, `2X-Classic`, `1X-General`, `2X-General`, `4X-General`, `8X-General`, `16X-General`, `1X-Compute`, `2X-Compute`, `4X-Compute`, `8X-Compute`, `16X-Compute`, `1X-Memory`, `2X-Memory`, `4X-Memory`, `8X-Memory`, `16X-Memory` | `"Standard-1X"` **timeout** | _integer_ | maximum number of seconds the task may run before being forced to exit **default:** `86400` (triggers created in the dashboard default to `1800`) **min:** `60` (`240` for Private, Shield, and the `*-Classic`, `*-General`, `*-Compute`, and `*-Memory` dyno sizes) **max:** `86400` | `1800` ##### Trigger Create Create a trigger that executes a task at a given interval or a specific moment in time. ``` POST /triggers ``` **Required Parameters** Name | Type | Description | Example -----|------|-------------|-------- **name** | _string_ | name of trigger | `"Trigger created via API"` **frequencyType** | _string_ | **one of:** `one-off` or `recurring` | `"recurring"` **schedule** | _string_ | cron expression or date and time | `"0 0 * * *"` **value** | _string_ | command or process type to execute | `"npm run something"` **timezone** | _string_ | IANA time zone name | `"America/New_York"` **state** | _string_ | **one of:** `active` or `inactive` | `"active"` **dyno** | _string_ | one-off dyno size | `"Standard-1X"` **Optional Parameters** Name | Type | Description | Example -----|------|-------------|-------- **timeout** | _integer_ | **default:** `86400` | `1800` **Curl Example** ```term $ curl -X POST https://api.advancedscheduler.io/triggers \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "name": "Trigger created via API", "frequencyType": "recurring", "schedule": "0 0 * * *", "value": "npm run something", "timezone": "America/New_York", "state": "active", "dyno": "Standard-1X", "timeout": 1800 }' ``` **Response Example** `200 OK` ```json { "message": "Trigger has been created!", "code": 200, "trigger": { "uuid": "", "name": "Trigger created via API", "resourceUUID": "", "frequencyType": "recurring", "schedule": "0 0 * * *", "value": "npm run something", "timezone": "America/New_York", "state": "active", "dyno": "Standard-1X", "timeout": 1800 } } ``` ##### Trigger Info Get an existing trigger. ``` GET /triggers/{trigger_uuid} ``` **Curl Example** ```term $ curl https://api.advancedscheduler.io/triggers/ \ -H "Authorization: Bearer " ``` **Response Example** `200 OK` ```json { "message": "Trigger found!", "code": 200, "trigger": { "uuid": "", "name": "Trigger created via API", "resourceUUID": "", "frequencyType": "recurring", "schedule": "0 0 * * *", "value": "npm run something", "timezone": "America/New_York", "state": "active", "dyno": "Standard-1X", "timeout": 1800 } } ``` ##### Trigger List List all existing triggers. ``` GET /triggers ``` **Curl Example** ```term $ curl https://api.advancedscheduler.io/triggers \ -H "Authorization: Bearer " ``` **Response Example** `200 OK` ```json { "message": "2 triggers found!", "code": 200, "triggers": [ { "uuid": "", "name": "Trigger created via API", "resourceUUID": "", "frequencyType": "recurring", "schedule": "0 0 * * *", "value": "npm run something", "timezone": "America/New_York", "state": "active", "dyno": "Standard-1X", "timeout": 1800 }, { "uuid": "", "name": "Another trigger", "resourceUUID": "", "frequencyType": "recurring", "schedule": "0 0 * * *", "value": "npm run something-else", "timezone": "UTC", "state": "active", "dyno": "Standard-1X", "timeout": 1800 } ] } ``` ##### Trigger Update Update an existing trigger. The full trigger body is required, not just the fields being changed. ``` PUT /triggers/{trigger_uuid} ``` **Required Parameters** Name | Type | Description | Example -----|------|-------------|-------- **uuid** | _uuid_ | must match `{trigger_uuid}` in the path | `""` **name** | _string_ | name of trigger | `"Trigger updated via API"` **frequencyType** | _string_ | **one of:** `one-off` or `recurring` | `"recurring"` **schedule** | _string_ | cron expression or date and time | `"0 0 * * *"` **value** | _string_ | command or process type to execute | `"npm run something-else"` **timezone** | _string_ | IANA time zone name | `"America/New_York"` **state** | _string_ | **one of:** `active` or `inactive` | `"active"` **dyno** | _string_ | one-off dyno size | `"Standard-1X"` **Optional Parameters** Name | Type | Description | Example -----|------|-------------|-------- **timeout** | _integer_ | | `1800` **Curl Example** ```term $ curl -X PUT https://api.advancedscheduler.io/triggers/ \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "uuid": "", "name": "Trigger updated via API", "frequencyType": "recurring", "schedule": "0 0 * * *", "value": "npm run something-else", "timezone": "America/New_York", "state": "active", "dyno": "Standard-1X", "timeout": 1800 }' ``` **Response Example** `200 OK` ```json { "message": "Trigger has been updated!", "code": 200, "trigger": { "uuid": "", "name": "Trigger updated via API", "resourceUUID": "", "frequencyType": "recurring", "schedule": "0 0 * * *", "value": "npm run something-else", "timezone": "America/New_York", "state": "active", "dyno": "Standard-1X", "timeout": 1800 } } ``` ##### Trigger Delete Delete an existing trigger. ``` DELETE /triggers/{trigger_uuid} ``` **Curl Example** ```term $ curl -X DELETE https://api.advancedscheduler.io/triggers/ \ -H "Authorization: Bearer " ``` **Response Example** `200 OK` ```json { "message": "Trigger has been deleted!", "code": 200 } ``` #### Trigger Execution A single run of a trigger's task on a one-off dyno. Name | Type | Description | Example -----|------|-------------|-------- **uuid** | _uuid_ | unique identifier of the execution | `""` **name** | _nullable string_ | name of the one-off dyno that ran the execution, `null` if it could not be started | `"advanced-scheduler.3836"` **type** | _string_ | how the execution was started **one of:** `scheduled`, `manual`, or `api` | `"scheduled"` **status** | _string_ | current status of the execution **one of:** `triggered`, `task-succeeded`, `task-failed`, `task-timed-out`, or `failed` | `"task-succeeded"` **triggerUUID** | _uuid_ | unique identifier of the trigger this execution belongs to | `""` **requestedAt** | _date-time_ | when the execution was requested | `"2024-09-15T09:39:03.000Z"` **exitedAt** | _date-time_ | when the one-off dyno exited, present only once the task has finished | `"2024-09-15T09:39:06.000Z"` **exitStatus** | _nullable integer_ | process exit code the one-off dyno exited with, present only once the task has finished; `0` indicates success | `0` **error** | _string_ | reason the execution failed to start, present only when `status` is `failed` | `"Heroku's one-off dyno limit reached."` ##### Trigger Execution Create Trigger an out-of-schedule execution of an existing trigger. On-demand executions require a paid plan. ``` POST /triggers/{trigger_uuid}/executions ``` This endpoint takes no request body. **Curl Example** ```term $ curl -X POST https://api.advancedscheduler.io/triggers//executions \ -H "Authorization: Bearer " ``` **Response Example** `200 OK` ```json { "message": "Trigger executed!", "code": 200, "execution": { "uuid": "", "name": "advanced-scheduler.4340", "type": "api", "status": "triggered", "triggerUUID": "", "requestedAt": "2024-09-22T17:53:13.000Z" } } ``` ##### Trigger Execution Info Get a single execution of an existing trigger. ``` GET /triggers/{trigger_uuid}/executions/{execution_uuid} ``` **Curl Example** ```term $ curl https://api.advancedscheduler.io/triggers//executions/ \ -H "Authorization: Bearer " ``` **Response Example** `200 OK` ```json { "message": "Execution found!", "code": 200, "execution": { "uuid": "", "name": "advanced-scheduler.3836", "type": "scheduled", "status": "task-succeeded", "triggerUUID": "", "requestedAt": "2024-09-15T09:39:03.000Z", "exitedAt": "2024-09-15T09:39:06.000Z", "exitStatus": 0 } } ``` ##### Trigger Execution List List the latest executions of an existing trigger. ``` GET /triggers/{trigger_uuid}/executions ``` **Optional Parameters** Name | Type | Description | Example -----|------|-------------|-------- **limit** | _integer_ | number of executions to return **default:** `10` **max:** `100` | `25` **Curl Example** ```term $ curl "https://api.advancedscheduler.io/triggers//executions?limit=25" \ -H "Authorization: Bearer " ``` **Response Example** `200 OK` ```json { "message": "5 executions found!", "code": 200, "executions": [ { "uuid": "", "name": "advanced-scheduler.8107", "type": "scheduled", "status": "triggered", "triggerUUID": "", "requestedAt": "2021-02-14T07:57:28.000Z" }, { "uuid": "", "name": null, "type": "manual", "status": "failed", "triggerUUID": "", "requestedAt": "2021-02-14T07:55:04.095Z", "error": "Heroku's one-off dyno limit reached." }, { "uuid": "", "name": "advanced-scheduler.2006", "type": "manual", "status": "task-timed-out", "triggerUUID": "", "requestedAt": "2021-02-14T07:54:45.000Z", "exitedAt": "2021-02-14T07:56:01.000Z", "exitStatus": null }, { "uuid": "", "name": "advanced-scheduler.8780", "type": "manual", "status": "task-failed", "triggerUUID": "", "requestedAt": "2021-02-14T07:54:20.000Z", "exitedAt": "2021-02-14T07:54:22.000Z", "exitStatus": 127 }, { "uuid": "", "name": "advanced-scheduler.5594", "type": "scheduled", "status": "task-succeeded", "triggerUUID": "", "requestedAt": "2021-02-14T07:53:19.000Z", "exitedAt": "2021-02-14T07:53:22.000Z", "exitStatus": 0 } ] } ``` --- ## CLI ### Advanced Scheduler CLI plugin The Advanced Scheduler CLI is a Heroku plugin extending the Service API for managing task scheduling. It enables creation and management of triggers directly from the terminal on Heroku applications. **Installation** ``` $ heroku plugins:install advanced-scheduler ``` Or via npm: ``` $ npm install -g advanced-scheduler ``` **`heroku triggers`** Lists all Advanced Scheduler triggers for an application. Options: - `-a, --app=app` (required) — application to run command against - `-h, --help` — display CLI help - `-j, --json` — output triggers in JSON format Example: ``` $ heroku triggers -a example === 01234567-89ab-cdef-0123-456789abcdef (active): Monday morning newsletter At 09:00 AM, only on Monday (UTC) w/ Standard-1X ⬢ $ node bin/send-newsletter.js ``` **`heroku triggers:create`** Creates a new Advanced Scheduler trigger. Required options: - `-a, --app=app` — application identifier - `--name=name` — trigger identifier - `--frequencyType=recurring|one-off` — execution frequency - `--schedule=schedule` — execution timing (cron format or datetime) - `--value=value` — command to execute - `--dyno=` — dyno class (Eco, Basic, Standard, Performance, Private, Shield variants) Optional flags: - `--state=active|inactive` (default: active) - `--timeout=timeout` (default: 1800 seconds) - `--timezone=timezone` (default: UTC) Examples: ``` $ heroku triggers:create -a example --name "Trigger created via CLI" \ --frequencyType recurring --schedule "* * * * *" \ --value "npm run something" --dyno Standard-1X $ heroku triggers:create -a example --name "Trigger created via CLI" \ --frequencyType one-off --schedule "2025-12-25 00:00:00" \ --value "npm run something" --dyno Standard-1X ``` **`heroku triggers:update `** Updates an existing trigger configuration. Arguments: - `` — trigger identifier Options: - `-a, --app=app` (required) - `--name=name` — new trigger name - `--frequencyType=recurring|one-off` — frequency mode - `--schedule=schedule` — new execution schedule - `--value=value` — new command - `--dyno=` — dyno class - `--state=active|inactive` — activation status - `--timeout=timeout` — execution timeout - `--timezone=timezone` — timezone setting Examples: ``` $ heroku triggers:update 01234567-89ab-cdef-0123-456789abcdef -a example \ --name "Trigger updated via CLI" --frequencyType recurring \ --schedule "* * * * *" --value "npm run something-else" --dyno Standard-1X $ heroku triggers:update 01234567-89ab-cdef-0123-456789abcdef -a example \ --name "Trigger updated via CLI" --frequencyType one-off \ --schedule "2025-12-25 00:00:00" --value "npm run something-else" --dyno Standard-1X ``` **`heroku triggers:activate `** Activates a deactivated trigger. Options: - `-a, --app=app` (required) - `-f, --force` — skip confirmation - `-h, --help` — show help **`heroku triggers:deactivate `** Deactivates an active trigger without deletion. Options: - `-a, --app=app` (required) - `-f, --force` — skip confirmation - `-h, --help` — show help **`heroku triggers:delete `** Permanently removes a trigger. Options: - `-a, --app=app` (required) - `-h, --help` — show help Example: ``` $ heroku triggers:delete 01234567-89ab-cdef-0123-456789abcdef -a example ``` Note: the CLI README's own examples currently show `--dyno Free`, a dyno type no longer offered — this file substitutes `Standard-1X` above for accuracy; the source README itself hasn't been updated yet. ### Heroku CLI Plugin usage Advanced Scheduler provides a Heroku CLI plugin to create and manage triggers directly from the terminal using the Heroku CLI. To install the plugin: ``` $ heroku plugins:install advanced-scheduler ``` To start using the plugin: ``` $ heroku triggers --app example === 01234567-89ab-cdef-0123-456789abcdef (active): Monday morning newsletter At 09:00 AM, only on Monday (UTC) w/ Standard-1X ⬢ $ node bin/send-newsletter.js ``` To consult the plugin documentation: ``` $ heroku triggers -h List the Advanced Scheduler triggers for an app USAGE $ heroku triggers... OPTIONS -a, --app=app (required) app to run command against -h, --help show CLI help -j, --json output triggers in json format EXAMPLE $ heroku triggers -a example COMMANDS triggers:activate Activate an Advanced Scheduler trigger for an app triggers:create Create a new Advanced Scheduler trigger for an app triggers:deactivate Deactivate an Advanced Scheduler trigger for an app triggers:delete Permanently delete an Advanced Scheduler trigger for an app triggers:update Update an Advanced Scheduler trigger for an app ``` --- ## Monitoring and Troubleshooting ### Debugging tasks To debug a task, you have to check your application's logs. You can check your logs in real time in the Advanced Scheduler dashboard, use any of the logging add-ons available in the Heroku Elements marketplace or use the Heroku CLI. To check real-time logs in the Advanced Scheduler dashboard, navigate to the Execution Logs page by clicking the `View Execution Logs` button in the top navigation bar on the overview page. To check your real-time logs with the Heroku CLI, use the `heroku logs` command: ``` $ heroku logs -t -a -d advanced-scheduler 2020-01-15T14:10:16+00:00 heroku[advanced-scheduler.1]: State changed from created to starting 2020-01-15T14:10:16+00:00 app[advanced-scheduler.1]: Starting process with command `node bin/send-newsletter.js` 2020-01-15T14:10:19+00:00 app[advanced-scheduler.1]: Sending newsletters... 2020-01-15T14:10:27+00:00 app[advanced-scheduler.1]: done. 2020-01-15T14:10:28+00:00 heroku[advanced-scheduler.1]: State changed from up to complete ``` A running task is also visible with the `heroku ps` command: ``` $ heroku ps === advanced-scheduler (Free): node bin/send-newsletter.js (1) advanced-scheduler.1: up 2020/01/15 14:10:16 +0100 (~ 2s ago) ``` The task monitoring of Advanced Scheduler depends on the result of your task. Make sure your task exits with the right exit code (sometimes referred to as a return status or exit status). A successful task returns a 0, while an unsuccessful one returns a non-zero value. ### Execution Logs > This feature is currently in Beta. Advanced Scheduler can stream application logs from the one-off dynos that run your scheduled tasks directly to your browser in real-time. This feature is useful for testing new triggers or debugging failed task executions. By default, execution log streaming is disabled. To enable it, click the `View Execution Logs` button in the Advanced Scheduler dashboard, click `Enable Execution Logs…` and confirm. When enabled, Advanced Scheduler automatically creates a temporary Heroku log session when opening the Execution Logs view. Logs are streamed directly from Heroku to your browser using Heroku's log sessions. Advanced Scheduler servers do not process or store your logs. Only logs from one-off dynos started by Advanced Scheduler are displayed in the dashboard, other logs are omitted. Heroku log sessions provide a short window of recent log history and stream new logs as they occur. If the page is refreshed or the session expires, a new log session will be created and earlier logs may no longer be available. When you enable execution log streaming for your Heroku app, Advanced Scheduler appends the trigger UUID to the process type of the one-off dynos it starts. As a result, the dyno name will change from `advanced-scheduler.1234` to `advanced-scheduler-.1234`. This allows you to easily filter logs by specific triggers in the dashboard. If you disable execution log streaming, the trigger UUID will no longer be included in the dyno name. #### Log Retention and Limits The log view retains up to **10,000 lines** of logs in memory. When this limit is reached, the oldest logs are automatically removed as new logs arrive, ensuring optimal browser performance during long-running tasks. Execution logs in the Advanced Scheduler dashboard are streamed in real-time to your browser and are not persisted. For production-ready log persistence and analysis, we recommend using one of Heroku Elements' logging add-ons with your Heroku app. #### Controlling the Log Stream You can control the log stream using the **Stop Stream** and **Start Stream** buttons in the log view: - **Stop Stream**: Stops receiving new logs, allowing you to review the current logs without new entries appearing. Any logs emitted while the stream is stopped are not captured. - **Start Stream**: Resumes streaming new logs. This feature is particularly useful when testing triggers or investigating specific log entries. You can run a task, let logs accumulate, stop the stream to analyze the output, and then start the stream again when you're ready to capture more logs. ### Task monitoring By default, Advanced Scheduler monitors the executions of your scheduled tasks. For every trigger an email notification is sent on the first failed execution each day. Note that the successful or failed execution of your task depends on the process exit code (sometimes referred to as a return status or exit status), so make sure your process is exiting properly. When the process exits with code 0, the execution is considered successful. Anything else is treated as a failed execution. Whenever a one-off dyno fails to be provisioned, the process never actually runs and consequently there is no process exit code. Advanced Scheduler interprets this as a failed execution and sends a task failure alert notification with exit status `null`. #### Alert Notification Subscribers By default, the distribution for email notifications is to all app owners and collaborators for non-org accounts, and admins for people in a Heroku Enterprise org. Alternatively, reach out to support@advancedscheduler.io to add additional email addresses, such as for email-based PagerDuty integration. ### Advanced Scheduler and Container Registry If you are using Advanced Scheduler and Container Registry as your deployment method, your task must be accessible from the `web` image. There is no way to specify a non-web image for task execution. ### Status page Link only — a live status widget, not static documentation: https://advancedscheduler.statuspage.io/ (current and historical service availability). --- ## Limits and Operational Behavior ### Concurrent one-off dyno limits The limit for your app's concurrently running one-off dynos depends on several factors. See which limit applies to your situation in the Heroku documentation on dyno-scaling-and-process-limits. Beware that when exceeding your app's concurrent one-off dyno limit, the next task might not be executed. To stay below Heroku's concurrent one-off dyno limit, make sure to plan the execution of your tasks with care. Avoid a backlog of one-off dynos created when scheduled tasks are executed before running tasks are finished or time out. Alternatively, you can contact Heroku to get your limit raised. ### Migrating between plans Note that application owners should carefully manage the migration timing to ensure proper application function during the migration process. Use the `heroku addons:upgrade` command to migrate to a new plan: ``` $ heroku addons:upgrade advanced-scheduler:newplan -----> Upgrading advanced-scheduler:newplan to sharp-mountain-4005... done, v18 ($60/mo) Your plan has been updated to: advanced-scheduler:newplan ``` When downgrading to a plan that does not include access to the Advanced Scheduler Service API, the API token will be removed if applicable. This action will also remove the config var `ADVANCED_SCHEDULER_API_TOKEN` and cause your Heroku application to restart. ### Removing the add-on You can remove Advanced Scheduler via the CLI: ``` $ heroku addons:destroy advanced-scheduler -----> Removing advanced-scheduler from sharp-mountain-4005... done, v20 (free) ``` **This will destroy all associated data and cannot be undone!** --- ## Heroku Scheduler: Migration and Comparison ### Import Heroku Scheduler Jobs Heroku Scheduler jobs can be imported into Advanced Scheduler using the Advanced Scheduler dashboard. Advanced Scheduler is totally compatible with Heroku Scheduler. All imported jobs will behave exactly the same after import. To start importing your Heroku Scheduler jobs, head to the Advanced Scheduler dashboard and click the `Import Heroku Scheduler Jobs` button in the Triggers section of the overview page. All imported jobs will be inactive to avoid collisions with your existing Heroku Scheduler jobs. #### Heroku API token Advanced Scheduler uses a Heroku API token to access the Heroku Scheduler jobs on your Heroku app. The Heroku API token can be retrieved using the `heroku auth:token` command, which outputs your current Heroku CLI authentication token and when it will expire. The token can be explicitly invalidated by running `heroku auth:logout`. The provided Heroku API token will not be stored by Advanced Scheduler and will only be used to perform a single GET request to fetch your Heroku Scheduler jobs. #### Heroku Scheduler Timeouts All jobs run by Heroku Scheduler have a maximum runtime equal to the frequency of their execution. For example, jobs scheduled to run every 10 minutes will be terminated after approximately 10 minutes. To make sure imported jobs behave identically, Advanced Scheduler uses the same timeout values that Heroku Scheduler uses. ### Heroku Scheduler documentation Link only — the separate, Heroku-provided Scheduler add-on's own docs, not Advanced Scheduler content: https://devcenter.heroku.com/articles/scheduler --- ## Pricing and Support ### Plans and pricing Advanced Scheduler offers five paid tiers plus a free trial, with pricing based on hourly rates capped at monthly maximums: | Plan | Price | Triggers | Executions | History | |------|-------|----------|-----------|---------| | Trial | Free | 3 | Up to 100 | 3 days | | Standard 0 | ~$0.021/hr (max $15/mo) | 12 | Unlimited | 3 days | | Standard 1 | ~$0.042/hr (max $30/mo) | 24 | Unlimited | 7 days | | Standard 2 | ~$0.083/hr (max $60/mo) | 60 | Unlimited | 14 days | | Standard 3 | ~$0.167/hr (max $120/mo) | 120 | Unlimited | 14 days | | Standard 4 | ~$0.333/hr (max $240/mo) | 240 | Unlimited | 30 days | All tiers include: one-off and recurring task scheduling, manual executions, real-time monitoring, execution logs, notifications, variable dyno types, timezone support, Service API access, Heroku CLI plugin, dedicated support, and maximum 30-second execution delay. **Regional availability** Common Runtime: United States and Europe. Private Spaces: Dublin, Frankfurt, London, Montreal, Mumbai, Oregon, Singapore, Sydney, Tokyo, Virginia. Advanced Scheduler is not shareable across apps — single install per application. ### Support All Advanced Scheduler support and runtime issues should be submitted via one of the Heroku Support channels. Any non-support related issues or product feedback is welcome at support@advancedscheduler.io. ### Other support links Link only: - Heroku Support (all runtime issues): https://help.heroku.com/ - Product feedback (feature requests, non-support): https://www.advancedscheduler.io/feedback --- ## Optional Link only — generic Heroku platform documentation, not authored by or specific to Advanced Scheduler: - Working with one-off dynos: https://devcenter.heroku.com/articles/one-off-dynos - Dyno types: https://devcenter.heroku.com/articles/dyno-types - Heroku limits: https://devcenter.heroku.com/articles/limits - Scheduled jobs and custom clock processes: https://devcenter.heroku.com/articles/scheduled-jobs-custom-clock-processes - Managing add-ons: https://devcenter.heroku.com/articles/managing-add-ons