Control the deployment lifecycle with Resource Hooks: PreSync hooks for database migrations, PostSync for smoke tests, SyncFail for rollbacks, and controlling their execution order.

In episode 12 we secured secrets so they can live in GitOps. But there's a question hanging since the start of the series: how do we run additional actions around the sync process? Applications that run a database migration before release, a smoke test after deployment, or a backup before a big change — all of those need coordinated execution, not just "apply the manifests". In this episode we discuss Resource Hooks: ArgoCD's mechanism for running jobs at specific points in the sync lifecycle.
Why does this matter? A good deployment isn't just placing the right image, it also handles the side effects: the database schema, health verification, notifications, and even recovery on failure. Hooks are how ArgoCD injects that logic without leaving the GitOps paradigm — every hook lives in Git as an ordinary manifest.
Every ArgoCD sync runs through five sequential phases:
| Phase | Position | Example usage |
|---|---|---|
| PreSync | Before the main resources | DB migration, backup, validation |
| Sync | Main resources are deployed | Deployment, Service, ConfigMap |
| PostSync | After the main resources | Smoke test, notification, cache initialization |
| SyncFail | Only when a phase above fails | Rollback, alert |
| Skip | Hook failed → whole sync canceled | Gate for certain conditions |
A hook is a resource (usually a Job) marked with a special annotation. ArgoCD runs it in the corresponding phase, and the sync only moves to the next phase if all hooks in the current phase succeed.
The most common way to create a hook is a Kubernetes Job with the hook annotation:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
namespace: billing-prod
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: ghcr.io/devnull/billing-api:1.2.0
command: ["bundle", "exec", "rails", "db:migrate"]When a sync starts, ArgoCD sees the argocd.argoproj.io/hook: PreSync annotation and runs this job before deploying the main resources. If the job finishes with exit code 0, the sync continues; if it fails, the sync stops at the PreSync phase.
Because a hook is only needed during sync, ArgoCD needs to know when to clean it up. The argocd.argoproj.io/hook-delete-policy annotation controls this:
| Policy | Effect |
|---|---|
BeforeHookCreation | Delete the hook from the previous sync before the new hook is created |
HookSucceeded | Delete the hook after it succeeds |
HookFailed | Delete the hook after it fails |
Combinations are separated by commas, e.g. HookSucceeded, HookFailed — the hook is cleaned up regardless of the outcome.
The most classic pattern: run the migration before the new Deployment comes up, so the schema and the application change in one atomic operation. The db-migrate job above is an example.
For risky changes, run a PreSync hook that backs up the database or volumes to separate storage — so there's a safety net before the new manifests are applied.
After the deployment finishes, run a PostSync hook that calls the application endpoint and verifies the response — for example checking that GET /health returns 200:
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-test
namespace: billing-prod
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: check
image: curlimages/curl:latest
command: ["curl", "-f", "https://api.billing.example.com/health"]A hook can be the spearhead of simple alerting — for example a SyncFail hook that sends a message to chat, although for this need ArgoCD notifications (episode 17) are usually cleaner.
If a sync fails, a SyncFail hook can execute a recovery action — for example shutting off traffic, or running a rollback script from the previous version image.
Warning
Hooks run in the context of the destination namespace, not the cluster where ArgoCD lives — except for hooks targeting cluster-level resources. Make sure the image and job permissions are available on the destination cluster.
Hooks (and every resource) can get an argocd.argoproj.io/sync-wave annotation to control execution order:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/sync-wave: "1"The rules: resources with a smaller wave run first; resources with the same wave run sequentially for hooks and may run in parallel for regular sync resources. A real example: DB migration (wave -1) before the Deployment (wave 0), then the smoke test (wave 2). Within a single wave, hooks run in the order they appear in the manifest.
Tip
Use negative waves for work that must precede everything (e.g. -1 for migration, -2 for backup) and positive waves for work that must follow (smoke test). That way the default resources without an annotation (wave 0) don't need to be changed.
When a hook fails, check its status and logs:
argocd app get billing --show-operation
kubectl get jobs -n billing-prod -l app.kubernetes.io/part-of=billing
kubectl logs job/db-migrate -n billing-prod --tail=100
kubectl describe job/db-migrate -n billing-prodargocd app get billing --show-operation shows the result of each hook phase; if a hook is stuck, kubectl describe job usually shows the cause right away (image missing, restarts, permissions). If a hook leaves old Jobs behind due to a wrong policy, clean them up manually with kubectl delete job --all -n billing-prod.
hook-delete-policy, its Jobs and Pods pile up in the namespace. Always set a policy.BeforeHookCreation, the migration job from the previous sync could run again and fail. Combine it with the right policy.This episode completed our understanding of the sync lifecycle: the five phases (PreSync, Sync, PostSync, SyncFail, Skip), Job-based hook implementation with the argocd.argoproj.io/hook annotation, hook deletion policies, use cases for DB migration, smoke tests, backup, notifications, and rollback, ordering with sync waves, and how to debug when a hook misbehaves.
The points you should take with you:
argocd.argoproj.io/hook-delete-policy prevents Jobs from piling up.argocd app get --show-operation and kubectl logs.There are times when we don't want a sync to happen — for example maintenance hours or a release freeze period. In the next episode 14 we discuss Sync Windows & Scheduling: cron-based allow/deny windows, maintenance windows, change freezes, and manual overrides for emergency deployments. See you in episode 14!