Skip to content

About

Terraform module: terraform-servicenow-scheduled-job

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

⏰ ServiceNow Scheduled Job Terraform Module

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 the tylerhatton/servicenow provider v0.11.x.

Terraform servicenow module type resources


🧩 Overview

  • ⏰ Creates one servicenow_scheduled_job record (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, and business_calendar — each validated against a closed set.
  • 🔒 Secure default: the empty call lands an active record whose run_type is on_demand — the job never fires on its own schedule until you choose a real frequency and set the matching run_* 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, and protection_policy for IP protection on distribution.
  • 🆔 Emits the keystone sys_id plus name, resolved schedule attributes, and scope for 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.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


🗺️ Where this fits in the family

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
Loading

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.


🧬 What this module builds

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
Loading
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.

📁 Module Structure

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

⚙️ Quick Start

# 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.


🔑 Required ServiceNow Roles

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.

⚠️ Because sysauto_script requires admin, 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_script can be customized per instance. Confirm the required role against the target instance's sys_security_acl records before granting access, and route any change to the stock ACL through Security/Compliance review.


📋 ServiceNow Prerequisites

  • 🧱 Target application scope — a scoped application (sys_scope) must exist; pass its identifier as scope. Provision it with terraform-servicenow-application and wire module.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 script reaches external systems, the referenced credential/connection records (e.g. via terraform-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 no provider {} block.

🔌 Typical wiring

Derived from the SCOPE.md Emits table — one row per output in outputs.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)

🧠 Architecture Notes

  • 🆔 sys_id is a 32-char hex record id and is INSTANCE-SPECIFIC. The same logical job has a different sys_id in 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.tf is a pure mapping of typed inputs onto one provider record — no business logic, no child records, no count/for_each.
  • 🧩 Provider-computed fields. run_start and run_dayofmonth are populated server-side when omitted (run_start → creation time, run_dayofmonth → 1). Leaving them null is expected; the resolved value surfaces in the outputs after apply.
  • ⌛ Datetime form is positional, not absolute. run_time and run_period use the provider's 1970-01-01 HH:MM:SS convention 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. Setting run_type = "weekly" without run_dayofweek, or "once" without run_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. scope is required so records never land silently in Global. To deliberately target Global, pass the Global scope's identifier explicitly (documented opt-out). Changing scope after 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 script body 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_job as force-new — name, scope, run_type, and every run_*/condition/protection_policy argument are updated in place via the provider's REST PATCH against the existing sysauto_script record. Terraform will never propose a destroy/recreate for this resource from a plain attribute change.

📚 Example Library (copy-paste)

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
}

📥 Inputs (high-level)

  • 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, default true), conditional (bool, default false), condition.
  • Distribution — protection_policy ("" | "read" | "protected", default "").

🧾 Outputs

  • sys_id — (primary) the job's 32-char sysauto_script record id; instance-specific; the cross-resource reference and the value terraform import consumes.
  • 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 to 1 when unset).
  • protection_policy — resolved IP-protection policy.
  • scope — the application scope the job landed in.

ℹ️ No outputs are sensitive and none are conditional (try(..., null)) — this module emits no secrets, by design.


🧱 Design Principles

  • 📦 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_type defaults to "on_demand" so an empty call produces a job that never fires on its own. Opt-out: set a real run_type and the matching run_* field.
  • 🔐 Secrets out of band. The module accepts no credential variables and emits no secrets. The script body 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, no count.
  • ✅ Closed value sets are validated. run_type, run_dayofweek, run_dayofmonth, and protection_policy reject out-of-range values at plan time.
  • 🆔 sys_id-first contract. The keystone sys_id plus key resolved attributes flow out for composition and audit.

🚀 Runbook

# 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.


🔍 Troubleshooting

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.

🔗 Related Docs

  • Terraform Registry — tylerhatton/servicenow provider: servicenow_scheduled_job resource & data source.
  • ServiceNow product docs — Scheduled Script Executions (sysauto_script), run types, and the datetime conventions for run_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 the script can call).

💙 "Infrastructure as Code should be standardized, consistent, and secure."

About

Terraform module: terraform-servicenow-scheduled-job

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages