Manages a single scheduled background script job (
sysauto_script) as a typed, scope-targeted, secure-by-default Terraform record — frequency, run window, gating condition, and IP-protection policy all expressed as validated inputs. Built for thetylerhatton/servicenowprovider v0.11.x.
- ⏰ Creates one
servicenow_scheduled_jobrecord (this) — a server-side background script that runs on a schedule you define. - 🗓️ Supports every provider
run_type:on_demand,daily,weekly,monthly,once,periodically, andbusiness_calendar— each validated against a closed set. - 🔒 Secure default: the empty call lands an
activerecord whoserun_typeison_demand— the job never fires on its own schedule until you choose a real frequency and set the matchingrun_*field. - 📦 Scope is required by design — records must land in an explicit scoped application, never silently in Global.
- 🧰 Optional gating condition (
conditional+condition), run window fields, andprotection_policyfor IP protection on distribution. - 🆔 Emits the keystone
sys_idplusname, resolved schedule attributes, andscopefor downstream auditing and reference.
💡 Why it matters: scheduled jobs run privileged server-side automation on their own clock — typing the schedule, forcing an explicit scope, and defaulting to no auto-run turns a high-blast-radius platform feature into a reviewable, least-surprise Terraform record.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
terraform-servicenow-scheduled-job is a scripting/automation module — it consumes a scope from the application module that owns it, and its script body commonly calls sibling script-include libraries and looks up credential/connection records at runtime (never passed through this module as a typed input).
flowchart LR
app[terraform-servicenow-application<br/>scoped application]
job[terraform-servicenow-scheduled-job<br/>scheduled script]
scriptinc[terraform-servicenow-script-include<br/>reusable script libraries]
cred[terraform-servicenow-credential<br/>runtime credentials]
conn[terraform-servicenow-connection<br/>runtime connections]
app -->|scope sys_id| job
scriptinc -.->|called at runtime by script body| job
cred -.->|looked up at runtime, never passed through| job
conn -.->|looked up at runtime| job
style job fill:#81B5A1,color:#fff
terraform-servicenow-scheduled-job consumes a scope sys_id from terraform-servicenow-application; its script body may call terraform-servicenow-script-include libraries and look up terraform-servicenow-credential/terraform-servicenow-connection records at runtime (dotted edges — not a typed Terraform input). It emits a sys_id for any module/record referencing the job.
flowchart TD
subgraph inputs[Inputs]
name[name]
script[script]
scope[scope sys_id]
runtype[run_type + run_* fields]
cond[condition / conditional]
pp[protection_policy]
end
job["servicenow_scheduled_job.this<br/>(sysauto_script) — keystone"]
name --> job
script --> job
scope --> job
runtype --> job
cond --> job
pp --> job
job --> out1([sys_id / name output])
job --> out2([run_type / run_start / run_dayofmonth])
job --> out3([scope / protection_policy])
style job fill:#81B5A1,color:#fff
| Resource | Backing table | Cardinality | Notes |
|---|---|---|---|
servicenow_scheduled_job.this |
sysauto_script |
1 (keystone) | Thin, total projection of every typed input onto a single record — no child records, no count/for_each. |
terraform-servicenow-scheduled-job/
├── providers.tf # required_providers only — servicenow = 0.11.0; no provider {} block
├── variables.tf # name, script, scope, + optional run_*/condition/protection_policy inputs
├── main.tf # servicenow_scheduled_job.this — thin total projection of the inputs
├── outputs.tf # sys_id (primary) + name, active, run_type, run_start, run_dayofmonth, protection_policy, scope
├── README.md # this file
└── SCOPE.md # cross-module contract: required roles, prerequisites, Emits table
# The CALLER configures the provider — instance + basic auth come from
# SERVICENOW_* env vars or a secret store, never from this module.
provider "servicenow" {
# instance_url = env SERVICENOW_INSTANCE_URL
# username = env SERVICENOW_USERNAME
# password = env SERVICENOW_PASSWORD (sensitive)
}
module "nightly_cleanup" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Nightly Cache Cleanup"
scope = module.application.sys_id # wire the scoped application's sys_id — never Global
script = <<-JS
var gr = new GlideRecord('sys_cache_flush');
gr.query;
while (gr.next) { gr.deleteRecord; }
JS
run_type = "daily"
run_time = "1970-01-01 02:00:00" # only the time portion is significant -> 02:00
}
⚠️ Pin the source with?ref=v1.0.0— never a branch.
Mandatory section — the ServiceNow analog of the AWS IAM-permissions list. Grant to a dedicated, least-privilege integration user, never a personal or all-powerful admin login.
| Role | Required for | Notes |
|---|---|---|
admin |
CRUD on sysauto_script (Scheduled Script Executions) |
ServiceNow gates scheduled-job records behind admin — there is no narrower delegated role for sysauto_script. Scope the integration user tightly and review its use. |
script capability (implied) |
Persisting executable script bodies |
Comes with admin; flagged so reviewers know this user can land server-side code. |
⚠️ Becausesysauto_scriptrequiresadmin, treat this integration user as elevated: restrict it to the sub-prod instance for local work, source its credentials from the secret store in CI, and keep its scope of managed records small.
ℹ️ ACLs on
sysauto_scriptcan be customized per instance. Confirm the required role against the target instance'ssys_security_aclrecords before granting access, and route any change to the stock ACL through Security/Compliance review.
- 🧱 Target application scope — a scoped application (
sys_scope) must exist; pass its identifier asscope. Provision it withterraform-servicenow-applicationand wiremodule.application.sys_id. - 🔌 No plugin/store app is required for core
sysauto_script— scheduled jobs ship with the base platform. - 🗓️ Business calendar — only if
run_type = "business_calendar"; the referenced calendar must already exist on the instance. - 🔐 Runtime credentials — if the
scriptreaches external systems, the referenced credential/connection records (e.g. viaterraform-servicenow-credential/terraform-servicenow-connection) must exist. Do not inline secrets in the script body. - 🌐 Instance + basic auth — provider-inherited via
SERVICENOW_*; this module declares noprovider {}block.
Derived from the
SCOPE.mdEmits table — one row per output inoutputs.tf.
| This module output | Feeds into |
|---|---|
sys_id |
Any module or record referencing this job by sys_id; the value terraform import consumes |
name |
The servicenow_scheduled_job data source lookup (by name); human-readable references |
active |
Audit/compliance reporting on whether automation is live |
run_type |
Schedule-posture review and drift checks |
run_start |
Auditing the resolved (server-computed) start time |
run_dayofmonth |
Auditing the resolved (server-computed) monthly day |
protection_policy |
IP-protection / distribution-policy review |
scope |
Confirming the job landed in the intended scoped application (not Global) |
- 🆔
sys_idis a 32-char hex record id and is INSTANCE-SPECIFIC. The same logical job has a differentsys_idin dev, test, and prod. Treat it as the cross-resource reference type (the ServiceNow analog of an AWS ARN). To adopt an existing record on a given instance:
terraform import servicenow_scheduled_job.this <sys_id>- 🧮 Thin total projection.
main.tfis a pure mapping of typed inputs onto one provider record — no business logic, no child records, nocount/for_each. - 🧩 Provider-computed fields.
run_startandrun_dayofmonthare populated server-side when omitted (run_start→ creation time,run_dayofmonth→1). Leaving themnullis expected; the resolved value surfaces in the outputs after apply. - ⌛ Datetime form is positional, not absolute.
run_timeandrun_perioduse the provider's1970-01-01 HH:MM:SSconvention where only the relevant portion matters (run_time= time of day;run_period= elapsed interval, e.g.1970-01-02 00:00:00= every 24h). - 🔁
run_type↔run_*coupling is by convention, not provider-enforced. Settingrun_type = "weekly"withoutrun_dayofweek, or"once"withoutrun_start, will apply but won't behave as intended. Set the matching field; the variable validations guard value ranges, not cross-field presence. - 📦 Scope behavior.
scopeis required so records never land silently in Global. To deliberately target Global, pass the Global scope's identifier explicitly (documented opt-out). Changingscopeafter creation moves the record between applications — review carefully. - 🔐 Basic-auth constraint. The provider authenticates with HTTP basic auth configured by the caller; this module never accepts credential variables and never emits secrets. The
scriptbody is not a secret store — it lands in state and platform source control. - 🧷 No immutable / recreate-on-change fields. The provider schema marks nothing on
servicenow_scheduled_jobas force-new —name,scope,run_type, and everyrun_*/condition/protection_policyargument are updated in place via the provider's REST PATCH against the existingsysauto_scriptrecord. Terraform will never propose a destroy/recreate for this resource from a plain attribute change.
1 · Minimal — secure default (on_demand, never auto-runs)
module "manual_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Manual Reindex"
scope = module.application.sys_id
script = "gs.info('Reindex triggered manually.');"
# run_type defaults to "on_demand" -> the job is active but never fires on its own.
}2 · Daily at a fixed time
module "daily_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Daily Metrics Roll-up"
scope = module.application.sys_id
script = file("${path.module}/scripts/metrics_rollup.js")
run_type = "daily"
run_time = "1970-01-01 06:30:00" # 06:30 each day
}3 · Weekly on a specific day
module "weekly_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Weekly Compliance Export"
scope = module.application.sys_id
script = file("${path.module}/scripts/compliance_export.js")
run_type = "weekly"
run_dayofweek = "1" # 1 = Monday … 7 = Sunday
run_time = "1970-01-01 07:00:00"
}4 · Monthly on a specific day
module "monthly_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Month-End Statement Sweep"
scope = module.application.sys_id
script = file("${path.module}/scripts/month_end.js")
run_type = "monthly"
run_dayofmonth = "1" # 1st of every month
run_time = "1970-01-01 03:00:00"
}5 · One-time run at a fixed datetime
module "once_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "One-Time Data Backfill"
scope = module.application.sys_id
script = file("${path.module}/scripts/backfill.js")
run_type = "once"
run_start = "2026-07-01 00:00:00" # required in practice for "once"
}6 · Periodically — every N (interval form)
module "periodic_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Queue Drain"
scope = module.application.sys_id
script = file("${path.module}/scripts/queue_drain.js")
run_type = "periodically"
run_period = "1970-01-01 00:05:00" # every 5 minutes
}7 · Business-calendar driven
module "calendar_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Business-Day Settlement"
scope = module.application.sys_id
script = file("${path.module}/scripts/settlement.js")
run_type = "business_calendar" # the referenced calendar must already exist on the instance
run_time = "1970-01-01 09:00:00"
}8 · Gated execution — conditional + condition
module "conditional_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Conditional Notifier"
scope = module.application.sys_id
script = file("${path.module}/scripts/notify.js")
run_type = "daily"
run_time = "1970-01-01 08:00:00"
conditional = true
condition = "gs.getProperty('casey.notifier.enabled') == 'true'" # gate before running
}9 · Staged (publish ahead of activation) — active = false
module "staged_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Q3 Migration Job"
scope = module.application.sys_id
script = file("${path.module}/scripts/migration.js")
run_type = "weekly"
run_dayofweek = "6"
run_time = "1970-01-01 22:00:00"
active = false # staged — enable deliberately when the migration window opens
}10 · IP-protected distribution — protection_policy
module "protected_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Packaged Maintenance Job"
scope = module.application.sys_id
script = file("${path.module}/scripts/maintenance.js")
run_type = "daily"
run_time = "1970-01-01 01:00:00"
protection_policy = "read" # "" (none, default) | "read" | "protected" — IP protection on distribution
}11 · Secure / least-privilege variant — explicit scope, gated, manual
module "hardened_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Sensitive Reconciliation"
scope = module.application.sys_id # explicit scoped application — never Global
# Credentials are looked up at runtime, NOT inlined:
script = <<-JS
var creds = new sn_cc.StandardCredentialsProvider;
var c = creds.getCredentialByID('casey_recon_credential');
//... use c.getAttribute('password') at runtime; nothing secret in state...
JS
run_type = "on_demand" # never auto-fires; triggered under human control
conditional = true
condition = "gs.getProperty('casey.recon.window_open') == 'true'"
}12 · End-to-end composition with terraform-servicenow-application
module "application" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-application?ref=v1.0.0"
#... scoped application definition...
}
module "nightly_job" {
source = "git::https://github.com/microsoftexpert/terraform-servicenow-scheduled-job?ref=v1.0.0"
name = "Nightly Job"
scope = module.application.sys_id # sibling sys_id reference
script = file("${path.module}/scripts/nightly.js")
run_type = "daily"
run_time = "1970-01-01 04:00:00"
}
output "nightly_job_sys_id" {
value = module.nightly_job.sys_id
}- Core / identity —
name(required, non-empty),script(required JavaScript body),scope(required application-scope identifier). - Schedule —
run_type(validated enum, default"on_demand"),run_time,run_start,run_period,run_dayofweek("1"–"7"),run_dayofmonth("1"–"31"). - Behavior —
active(bool, defaulttrue),conditional(bool, defaultfalse),condition. - Distribution —
protection_policy(""|"read"|"protected", default"").
sys_id— (primary) the job's 32-charsysauto_scriptrecord id; instance-specific; the cross-resource reference and the valueterraform importconsumes.name— natural key for the data-source lookup.active— whether the job is enabled.run_type— resolved schedule frequency.run_start— resolved start datetime (provider-computed when unset).run_dayofmonth— resolved monthly day (provider-computed, defaults to1when unset).protection_policy— resolved IP-protection policy.scope— the application scope the job landed in.
ℹ️ No outputs are
sensitiveand none are conditional (try(..., null)) — this module emits no secrets, by design.
- 📦 Scope is required (secure default). Records never land silently in Global. Opt-out: pass the Global scope identifier explicitly to
scope. - 🛑 No auto-run by default.
run_typedefaults to"on_demand"so an empty call produces a job that never fires on its own. Opt-out: set a realrun_typeand the matchingrun_*field. - 🔐 Secrets out of band. The module accepts no credential variables and emits no secrets. The
scriptbody is not a vault — reference credentials via a runtime lookup. No opt-out — this is absolute. - 🧱 One keystone, four files. Exactly one
servicenow_scheduled_job.this; no child records, nocount. - ✅ Closed value sets are validated.
run_type,run_dayofweek,run_dayofmonth, andprotection_policyreject out-of-range values at plan time. - 🆔
sys_id-first contract. The keystonesys_idplus key resolved attributes flow out for composition and audit.
# Static checks — no credentials required
terraform init -backend=false
terraform validate
terraform fmt -check
# Plan / apply / inspect — require SERVICENOW_* for a (sub-prod) instance
export SERVICENOW_INSTANCE_URL="https://<instance>.service-now.com"
export SERVICENOW_USERNAME="svc_terraform"
export SERVICENOW_PASSWORD="********" # from the secret store, never the repo
terraform plan
terraform apply
terraform output sys_id
⚠️ Pin the module source with?ref=v1.0.0, never a branch. CI runs plan-only; a human reviews and applies.
| Symptom | Likely cause | Resolution |
|---|---|---|
| 401 Unauthorized on plan/apply | Missing/wrong SERVICENOW_* credentials, or pointing at the wrong instance |
Confirm SERVICENOW_INSTANCE_URL/USERNAME/PASSWORD are exported for the run and resolve to the intended (sub-prod) instance. This module never sets credentials — the caller's provider does. |
| 403 Forbidden creating the job | Integration user lacks admin for sysauto_script |
Grant the least-privilege integration user admin (scheduled jobs have no narrower delegated role). Verify with a manual record create in the UI as that user. |
| Job created but never runs | run_type set without the matching run_* field (e.g. weekly with no run_dayofweek, once with no run_start) |
Cross-field coupling is convention, not provider-enforced. Set the field the chosen run_type needs. |
| Job runs but "shouldn't have" | Left at the default run_type = "on_demand" was expected, but a real frequency was set |
Confirm the intended posture; the secure default is on_demand (no auto-run). |
| Record landed in Global / wrong scope | scope pointed at the Global or an unintended application |
Pass the correct scoped-application identifier (module.application.sys_id). scope is required precisely to prevent silent-Global landings. |
run_start / run_dayofmonth show a value you didn't set |
These are provider-computed — ServiceNow fills them server-side (run_start → creation time, run_dayofmonth → 1) |
Expected. Read the resolved value from the outputs; don't fight the server default with a perpetual diff. |
| Import succeeds but plan shows changes | sys_id is instance-specific; you imported a record whose server-side values differ from your config |
Reconcile config to the live record (especially the computed run_* fields), or accept the one-time correcting apply. Use terraform import servicenow_scheduled_job.this <sys_id> against the correct instance. |
business_calendar job inert |
The referenced business calendar doesn't exist on the instance | Provision/activate the calendar first; run_type = "business_calendar" depends on it. |
- Terraform Registry —
tylerhatton/servicenowprovider:servicenow_scheduled_jobresource & data source. - ServiceNow product docs — Scheduled Script Executions (
sysauto_script), run types, and the datetime conventions forrun_time/run_period. - ServiceNow product docs — Application scopes (
sys_scope) and scoped-application development. - module library —
terraform-servicenow-application(scope provider),terraform-servicenow-credential/terraform-servicenow-connection(runtime credentials),terraform-servicenow-script-include(reusable server libraries thescriptcan call).
💙 "Infrastructure as Code should be standardized, consistent, and secure."

