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
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.
4. Refresh the agent sources
After changing your override repo, you’ll need to refresh the agent sources so it pulls the latest code.
Final result
With these overrides, your scheduled task scope will:
- Skip DNS creation and deletion
- Use a Kubernetes
CronJobinstead of aDeployment - 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/scheduled_task.