Skip to main content

Overriding a scheduled task scope

In this example, we’ll combine configuration and behavior overrides to build a scope for scheduled tasks applications that run on a schedule (via Cron), do their work, and exit.

Common uses include:

  • Background jobs
  • Report generation
  • Event processors
  • Any workload that doesn’t need to expose a network endpoint
info

We recommend first setting up the production-ready scheduled task scope to get a working baseline.
This guide then builds on that base implementation, overriding it with custom configuration and behavior.

Repo structure

Place workflow overrides under scope/workflows/ or deployment/workflows/ depending on the lifecycle action you're customizing.

your-override-repo/
├── scope/ # Scope lifecycle actions
│ └── workflows/
│ ├── create.yaml
│ ├── update.yaml
│ └── delete.yaml
├── deployment/
│ └── templates/
│ ├── deployment.yaml.tpl
│ └── workflows/
│ ├── initial.yaml
│ ├── blue_green.yaml
├── values.yaml # Your configuration overrides
└── ..

1. Skip DNS configuration

Scheduled jobs don’t need a service or DNS record. We can skip these steps in both the create and delete workflows.

Repo structure:

your-override-repo/
├── scope/ # Scope lifecycle actions
│ └── workflows/
│ ├── create.yaml
│ └── delete.yaml
└── ..

To do that, override the default create and delete workflows for the scope:

# path: scope/workflows/create.yaml
include:
- "$SERVICE_PATH/values.yaml"
steps:
- name: networking
action: skip
# path: scope/workflows/delete.yaml
include:
- "$SERVICE_PATH/values.yaml"
steps:
- name: networking
action: skip

2. Use a CronJob instead of a Deployment

In Kubernetes, scheduled jobs use CronJob objects. We’ll replace the base Deployment template with a CronJob template.

Repo structure:

your-override-repo/
├── deployment/
│ └── templates/
│ └── deployment.yaml.tpl
└── values.yaml # Your configuration overrides

Inside the template (deployment.yaml.tpl), add:

apiVersion: batch/v1
kind: CronJob
metadata:
name: job-{{ .scope.id }}-{{ .deployment.id }}
namespace: {{ .k8s_namespace }}
labels:
name: d-{{ .scope.id }}-{{ .deployment.id }}
app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }}
nullplatform: "true"
spec:
schedule: "{{ .scope.capabilities.cron }}"
concurrencyPolicy: {{ .scope.capabilities.concurrency_policy }}
successfulJobsHistoryLimit: {{ .scope.capabilities.history_limit }}
failedJobsHistoryLimit: {{ .scope.capabilities.history_limit }}
jobTemplate:
metadata:
labels:
name: d-{{ .scope.id }}-{{ .deployment.id }}
app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }}
nullplatform: "true"
spec:
backoffLimit: {{ .scope.capabilities.retries }}
template:
metadata:
labels:
name: d-{{ .scope.id }}-{{ .deployment.id }}
app.kubernetes.io/part-of: {{ .namespace.slug }}-{{ .application.slug }}
nullplatform: "true"
annotations:
nullplatform.logs.cloudwatch: 'true'
nullplatform.logs.cloudwatch.log_group_name: {{ .namespace.slug }}.{{ .application.slug }}
nullplatform.logs.cloudwatch.log_stream_log_retention_days: '7'
nullplatform.logs.cloudwatch.log_stream_name_pattern: >-
type=${type};application={{ .application.id }};scope={{ .scope.id }};deploy={{ .deployment.id }};instance=${instance};container=${container}
nullplatform.logs.cloudwatch.region: us-east-1
spec:
restartPolicy: OnFailure
securityContext:
runAsUser: 0
containers:
- name: application
image: {{ .asset.url }}
envFrom:
- secretRef:
name: s-{{ .scope.id }}-d-{{ .deployment.id }}
resources:
limits:
cpu: {{ .scope.capabilities.cpu_millicores }}m
memory: {{ .scope.capabilities.ram_memory }}Mi
requests:
cpu: {{ .scope.capabilities.cpu_millicores }}m
memory: {{ .scope.capabilities.ram_memory }}Mi

Then, point your values.yaml to this new template.

configuration:
DEPLOYMENT_TEMPLATE: "$OVERRIDES_PATH/deployment/templates/deployment.yaml.tpl"

3. Modify the deployment workflow

Because we’re no longer using Ingress or Service, we can skip the route traffic step and replace the deployment logic with one that applies our CronJob template.

Repo structure:

your-override-repo/
│ └── workflows/
│ └── initial.yaml
└── ..

In initial.yaml:

include:
- "$SERVICE_PATH/values.yaml"
steps:
- name: route traffic
action: skip

- name: create deployment
type: script
action: replace
file: "$OVERRIDES_PATH/deployment/build_deployment"
output:
- name: DEPLOYMENT_PATH
type: file
file: "$OUTPUT_DIR/deployment-$SCOPE_ID-$DEPLOYMENT_ID.yaml"
- name: SECRET_PATH
type: file
file: "$OUTPUT_DIR/secret-$SCOPE_ID-$DEPLOYMENT_ID.yaml"

- name: apply
type: script
file: "$SERVICE_PATH/apply_templates"
configuration:
ACTION: apply
DRY_RUN: false

post:
name: wait deployment active
action: skip

This ensures the correct manifest is generated and applied, without unnecessary networking logic.

Final result

With these overrides, your scheduled task scope will:

  • Skip DNS creation and deletion
  • Use a Kubernetes CronJob instead of a Deployment
  • Remove unnecessary networking steps
  • Still manage secrets, resources, and logs

This is a minimal working example — you can add metrics, custom logging, or even a force-run action if needed.

You'll find the complete implementation of this scope in our repo: https://github.com/nullplatform/scopes/tree/main/cronjob.