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
CronJob
instead 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.