Tools · Argo Events

Argo Events

Every other tool on this shelf reacts to something already sitting still — a Git commit Argo CD compares against, forever; a Rollout object Argo Rollouts steps through once it exists; a DAG Argo Workflows was handed to run. Argo Events reacts to something happening — a webhook firing, a file landing in a bucket, a message arriving on a queue, a cron tick — and its whole job is turning that moment into a Kubernetes object nothing asked for a second earlier. It is the smallest of the four Argo siblings, worth only 12% of the CAPA blueprint on its own, and the one most people meet last. It is also the piece that turns three independent reconcilers into one connected loop: a webhook can start a Workflow, a Workflow can commit a tag Argo CD reconciles, and a canary Argo Rollouts is halfway through can be paused or promoted by a signal from entirely outside the cluster. This page covers the three-piece architecture, the actual YAML you write for each piece, how a Sensor's filters and conditions decide whether to fire, the trigger types on the menu, day-to-day commands, the gotchas, and the comparison everyone reaches for and gets slightly wrong: Argo Events versus KEDA.

☺ Explain it like I'm 10

Picture a tiny, very fast mail courier who stands at a mailbox all day watching for letters to arrive — a webhook, a file dropped in a folder, an alarm clock going off at 6am. The instant a letter shows up, the courier doesn't open it or decide anything; she just flies it straight to a sorting desk and drops it in a tray. Sitting at that desk is a clerk with a very specific set of rules taped to the wall: "if a letter says 'push' AND it's from the right sender, stamp it and send it to the workshop." The clerk never watches the mailbox, and the courier never reads the rules on the wall — each does exactly one job, and the letter passing hand to hand is the only thing connecting them.

🐦Your host for this topic: Pip the Hummingbird — she usually carries live packets across the Cilium datapath the instant they leave a pod; here she's carrying something else across a different kind of wire, not traffic, but the fact that something happened at all.

Event-driven creation, not continuous reconciliation

☺ Like you're 10: Argo CD keeps checking its booklet forever, whether or not anything happened. Argo Events does nothing at all until something happens, and then it acts exactly once.

The Argo ecosystem covers how four independently-shipped projects share a name and little else, and Argo Events is where that independence is most visible, because its whole trigger is the opposite shape from its siblings'. Argo CD's application-controller and Argo Rollouts' controller both run a loop that never stops: poll, diff, decide, repeat, on a timer, forever, whether or not the world changed. Argo Events runs no such loop. It sits idle — genuinely idle, no polling, no periodic diff — until a real occurrence arrives from outside, and then it does one thing exactly once and goes back to waiting. That distinction is not a minor implementation detail; it's the reason Argo Events is the piece capable of starting the whole delivery loop in the first place. Something has to be the part that notices a pull request merged, and "continuously reconcile against a booklet" cannot be that part, because nothing wrote the booklet yet.

◆ Key idea

Three independent custom resources do the whole job, and each owns exactly one hop: an EventSource listens to the outside world, an EventBus carries what it hears, and a Sensor decides whether that's enough to act on and, if so, fires a trigger. None of the three can do either of the other two's jobs — a Sensor never listens to a webhook directly, and an EventSource never decides whether to act. That separation is what makes the pieces individually simple and the wiring between them almost mechanical to reason about.

Architecture: EventSource, EventBus, Sensor

☺ Like you're 10: One piece watches the mailbox, one piece is the sorting desk, one piece is the clerk who reads the rules and decides what happens next.

A standard install lands in its own argo-events namespace, and — unlike Argo CD's handful of separate controller Deployments — everything runs inside one controller-manager Deployment, reconciling EventBus, EventSource and Sensor objects alike from a single binary: kubectl get deploy -n argo-events shows exactly one entry, not three. What that one process actually does differs sharply by CRD, though, and it's worth keeping the two halves distinct in your head under the names the rest of this page uses — eventsource-controller and sensor-controller. For every EventSource object, it spins up a dedicated listener pod that does the actual watching: opening an HTTP port for a webhook, polling an SQS queue, subscribing to a Kafka topic, or firing on a cron schedule. For every Sensor object, it spins up a dedicated evaluator pod that subscribes to the events that Sensor depends on and runs its filters and conditions against them. Neither listener pod nor evaluator pod knows the other project exists; the only thing connecting an EventSource pod to a Sensor pod is the EventBus sitting between them.

The EventBus itself is self-managed Kubernetes-native infrastructure, not something you point at a service you already run. Out of the box it's backed by NATS: the classic native bus — still what the quickstart installs — runs NATS Streaming as a StatefulSet Argo Events manages for you inside argo-events; jetstream is the newer NATS backend the project is steering new installs toward, since NATS Streaming itself is being phased out upstream, and Kafka is also a supported backend on recent releases for platforms that already standardize on it. Whichever backend you pick, the bus is pure transport — it has no idea what a "push event" or a "signature check" means, it just moves messages between the two things that do. Delivery is at-least-once, not exactly-once, which matters more than it sounds like it should — see the gotchas below.

Event producers webhook calendar / cron resource (watch a CR) kafka / sqs minio (S3 notify) github / gitlab each runs its own listener pod EventBus NATS, self-managed ns: argo-events transport only Sensor dependencies filters: data / context conditions: AND / OR evaluates the payload, decides whether to fire fires at most once per event that passes Creates / calls Workflow (argoWorkflow) any object (k8s) HTTP call Slack message log (debug) one Sensor, many triggers publishes subscribes fires trigger One controller-manager, two jobs — the eventsource-controller role runs the listener pods, the sensor-controller role runs the clerk. Nothing here polls Git or cluster state — every hop starts because something happened, once.
⚠ The EventBus is a single point of silence

None of the other three Argo projects check on Argo Events, and Argo Events has no drift detector watching itself. If the EventBus goes down, every EventSource keeps accepting webhooks and every Sensor keeps its object sitting there looking healthy — but nothing gets published, nothing gets evaluated, and nothing fires. There is no error banner, no Degraded status propagating anywhere obvious; the first symptom most teams see is "why hasn't a Workflow started in two hours," and the fix is kubectl get eventbus -n argo-events, not a Workflow-side investigation.

The EventSource: what it watches

☺ Like you're 10: This is the courier's whole job description — which mailbox to stand at, and what counts as a letter worth carrying.

An EventSource can watch dozens of different kinds of thing, and each spec key names one type of listener it runs. The handful worth knowing cold:

TypeWhat it watchesWorth knowing
webhookAn HTTP endpoint it opens and exposes via a ServiceNo built-in signature verification — pair it with a Sensor data filter checking a shared secret, or reach for a typed source below instead
calendarA cron expression or a fixed intervalThe closest thing Argo Events ships to a scheduler — useful for a nightly trigger with no real external event behind it
resourceCreate/update/delete on Kubernetes objects matching a GVK and label selectorThe bridge into GitOps-adjacent triggers — react to a CR going Degraded, or a namespace appearing, without polling for it yourself
sqs / kafkaA queue or a topicA long-lived subscription the listener pod holds open, not a poll loop you tuned an interval for
minioBucket notifications on a MinIO serverThe listener pod subscribes directly via the MinIO client SDK — no separate bucket-side webhook to wire up. MinIO only, not raw AWS S3; use the sqs/sns sources for that
github / gitlabRepository webhooks, typedUnlike raw webhook, signature verification and event-type parsing are handled for you
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: git-webhook
  namespace: argo-events
spec:
  service:
    ports:
      - port: 12000
        targetPort: 12000
  webhook:
    push:                          # this key becomes the eventName a Sensor depends on
      port: "12000"
      endpoint: /push
      method: POST
    signature-ok:                  # a second endpoint on the same EventSource
      port: "12000"
      endpoint: /verify
      method: POST
---
apiVersion: argoproj.io/v1alpha1
kind: EventSource
metadata:
  name: nightly-report
  namespace: argo-events
spec:
  calendar:
    daily-run:
      schedule: "0 6 * * *"        # 06:00 UTC, every day
      timezone: "UTC"

Every key under a type block — push and signature-ok above, daily-run in the second — becomes an eventName a Sensor names in its own dependencies. One EventSource, one listener pod, can expose several independent events at once; a Sensor picks and chooses which of them it cares about.

The Sensor: dependencies, filters, and conditions

☺ Like you're 10: The clerk's rules, written down: which mailboxes to watch, what a letter has to say to count, and whether it needs one letter or several before doing anything.

A Sensor's dependencies array names the events it subscribes to — each entry pairs an eventSourceName with one of that source's eventNames, and gives the pair a short local name the rest of the Sensor refers back to. Each dependency can carry its own filters: a data filter reaches into the event payload by path, coerces it to a declared type, and compares it against one or more allowed values — the standard way to say "only main, not every branch." A context filter compares metadata about the event itself rather than its body. When a trigger depends on more than one dependency, its own conditions field is a small boolean expression over their names — "push && signed" for AND, "push || manual-run" for OR — deciding whether that specific trigger fires, independently of any other trigger the same Sensor might also define.

apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
  name: on-push
  namespace: argo-events
spec:
  dependencies:
    - name: push
      eventSourceName: git-webhook
      eventName: push
      filters:
        data:
          - path: body.ref
            type: string
            value:
              - "refs/heads/main"          # only main, not every branch
    - name: signed
      eventSourceName: git-webhook
      eventName: signature-ok
  triggers:
    - template:
        name: run-ci-pipeline
        conditions: "push && signed"        # both dependencies must fire — AND, not OR
        argoWorkflow:
          operation: submit
          source:
            resource:
              apiVersion: argoproj.io/v1alpha1
              kind: Workflow
              metadata:
                generateName: ci-
              spec:
                workflowTemplateRef:
                  name: ci-pipeline
          parameters:
            - src:
                dependencyName: push
                dataKey: body.after           # the commit SHA out of the webhook payload
              dest: spec.arguments.parameters.0.value

That last block, parameters, is the part that makes triggers more than a blunt on/off switch: it copies a value out of the event's own payload (src) into a field of the object being created (dest), so the Workflow that gets submitted actually carries the commit SHA that caused it, without a human or a separate step wiring that value through by hand.

Triggers: what actually gets created

☺ Like you're 10: Once the clerk's rules say "yes, act," this is the whole menu of things the clerk is allowed to do next.

Trigger typeWhat it doesTypical use
argoWorkflowSubmits, resumes, retries, or resubmits a WorkflowBy far the most common trigger — this is the CI-on-event pattern the whole page has been building toward
k8sCreates, patches, or deletes any Kubernetes object, genericallyAn escape hatch — bump a ConfigMap, patch a resource, create a Job with no Argo Workflows involved at all
httpCalls an arbitrary HTTP endpointNotify a system that doesn't live in the cluster at all
slackPosts a message to a Slack channelHuman-facing notification, not a workload — nothing gets created in the cluster
logPrints the matched event to the Sensor's own logDebugging a new EventSource/filter combination before wiring a real trigger behind it

One Sensor can define several triggers, each with its own conditions gating it independently against the same set of dependencies — one event can start a Workflow and post to Slack and patch a ConfigMap, from one Sensor object, with three different gates if you want them to differ.

✎ Try it

On a throwaway cluster, install Argo Events, a native EventBus, the git-webhook EventSource and the on-push Sensor above — minus the signed dependency and the conditions line, to start. Port-forward the EventSource's Service and curl its /push endpoint with a small JSON body by hand — no real GitHub webhook required. Watch a Workflow appear from nothing. Then add the signed dependency back and the conditions: "push && signed" line, curl only /push again, and confirm nothing fires until you also hit /verify — the exact AND-gate behavior the YAML above describes, made concrete in about ten minutes.

Argo Events vs. KEDA: two different meanings of "event-driven"

☺ Like you're 10: Argo Events makes brand-new things appear. KEDA makes more — or fewer — copies of a thing that's already there.

Both projects put "event-driven" in their pitch, and CAPA study guides warn about this collision for a reason: they solve genuinely different problems and share almost no mechanism. KEDA wraps an existing, already-running Deployment (or a Job-shaped workload) with a ScaledObject or ScaledJob, watches an external signal — queue depth, consumer lag, a PromQL result — and adjusts that workload's replica count, all the way to zero and back. Nothing new is ever created in the sense Argo Events means it; the same Deployment persists the whole time, and only the number attached to it moves. Argo Events never touches a replica count at all — every successful trigger produces a genuinely new object with its own identity, typically a Workflow, that did not exist a moment before and that KEDA has no concept of creating.

The one place the two get legitimately close is ScaledJob: unlike ScaledObject, it does create real Kubernetes Job objects, one per unit of queued work, with no HPA involved at all — which sounds a lot like "creation" until you look at what it creates. Every Job a ScaledJob spawns is identical, parameterless, and produced purely by counting a queue; it can't read a webhook payload, apply a filter, satisfy a boolean condition across several sources, or call Slack. A Sensor's trigger is shaped by the specific event that caused it. A ScaledJob's Job is shaped only by how many messages were waiting.

Argo Events — creates KEDA — scales event A event B event C new Workflow A new Workflow B new Workflow C three events, three separate objects that persist afterward checkout-worker · replicas: 0 checkout-worker · replicas: 1 checkout-worker · replicas: 6 same object the whole time — only the replica count moves
🦆 Dot's-eye view

"I spent an afternoon trying to make KEDA 'trigger a build' before someone pointed out I had the wrong tool entirely. KEDA answered a question I wasn't asking — 'how many of these should be running?' — when what I actually wanted was 'make one new thing happen, once, with this exact payload attached.' Once I said it out loud like that, it was obviously a Sensor, not a ScaledObject."

Day-to-day commands

☺ Like you're 10: There's no special app for this one — just plain kubectl against three kinds of object, plus reading logs to see whether a letter actually got delivered.

Unlike Argo CD, Argo Events ships no dedicated CLI binary — every day-to-day interaction is kubectl against its three CRDs, plus reading the right pod's logs.

# install (CRDs + the one controller-manager) and a native EventBus
$ kubectl create namespace argo-events
$ kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-events/stable/manifests/install.yaml
$ kubectl apply -n argo-events -f https://raw.githubusercontent.com/argoproj/argo-events/stable/examples/eventbus/native.yaml

$ kubectl apply -n argo-events -f eventsource-git-webhook.yaml
$ kubectl apply -n argo-events -f sensor-on-push.yaml

$ kubectl get eventbus,eventsource,sensor -n argo-events        # all three CRDs, one namespace
$ kubectl get pods -n argo-events                                # one pod per EventSource, one per Sensor, plus the bus

$ kubectl logs -n argo-events -l eventsource-name=git-webhook -f   # did the event even arrive here?
$ kubectl logs -n argo-events -l sensor-name=on-push -f            # did the conditions evaluate true?

$ kubectl port-forward -n argo-events svc/git-webhook-eventsource-svc 12000:12000
$ curl -X POST http://localhost:12000/push -d '{"ref":"refs/heads/main"}'   # fire it by hand, no real webhook needed

$ kubectl describe sensor on-push -n argo-events                 # last trigger time, per-dependency status

Gotchas and failure modes

☺ Like you're 10: Most surprises here come from something being genuinely silent by design, not from an error message you just haven't found yet.

At-least-once delivery means your trigger target must tolerate duplicates

The EventBus guarantees at-least-once delivery, not exactly-once — a listener pod restart, a slow acknowledgment, or a network blip can all cause the same event to reach a Sensor twice. A Workflow created with generateName shrugs this off by design, since a duplicate submission just becomes a second harmless Workflow run. A k8s trigger that patches a live object, or an http trigger calling a payment API, does not get that same free pass — idempotency has to be designed into whatever the trigger actually does, not assumed from the platform underneath it.

A Sensor's ServiceAccount needs its own RBAC for whatever it creates

A freshly installed Sensor has no inherent right to create a Workflow, patch a ConfigMap, or touch anything else — its pod runs under its own ServiceAccount, and that account needs a Role or ClusterRole granting exactly the verbs and resources its triggers use. Get this wrong and the failure is quiet in the place you'd expect it to be loud: the target namespace shows nothing happened, and the actual "forbidden" error sits in the Sensor pod's own log, not anywhere near the object you were expecting to see appear.

An unauthenticated raw webhook is a public trigger for anyone who finds the URL

A plain webhook EventSource has no built-in signature check — it fires on any POST to its endpoint that reaches it, from anyone who knows or guesses the URL. Treating the URL's obscurity as the security boundary is the mistake; the fix is a Sensor data filter checking a shared-secret header or field in every payload before any trigger fires, or reaching for a typed source like github/gitlab that verifies a real signature for you before the event ever reaches a Sensor's filters at all.

⚠ A trigger that bypasses Git fights the reconciler that owns that object

Because a Sensor's k8s trigger can create, patch, or delete any Kubernetes object, it's technically capable of writing straight to an Argo CD-managed Application or a live Rollout, skipping Git entirely. The moment it does, that object's live state disagrees with what Git says, and Argo CD's selfHeal — built specifically to notice and correct exactly that kind of disagreement — reverts the Sensor's own change on the very next reconcile pass. Route the change through Git like the rest of the loop does; a Sensor being fast is not the same thing as a Sensor being a legitimate shortcut around a reconciler built to undo shortcuts.

Argo Events vs. the alternatives

☺ Like you're 10: A few other tools also react to things happening — they just trade away the filters, the CRD-native shape, or the "anything can be a trigger" flexibility.

OptionModelBest whenCosts you
Argo EventsEvent-driven creation; EventSource/EventBus/Sensor; any Kubernetes object (or Slack, HTTP call) as the outputA discrete occurrence outside the cluster needs to start something new — a build, a notification, a one-off objectA third CRD family and a bus to run; at-least-once delivery means the trigger target must tolerate duplicates
KEDAEvent-driven scaling; ScaledObject/ScaledJob wrap the HPA, or spawn identical JobsAn existing long-running or job-shaped workload should track a queue or metric up and down, including to zeroNever creates anything genuinely new — only ever changes a replica count, or spawns parameterless Jobs, on a workload that already exists
Knative EventingA CloudEvents-native broker/trigger model for routing events at platform scaleThe whole platform is already built around CloudEvents and Knative ServingA far bigger platform commitment than three CRDs — adopting Knative's entire event model, not bolting one small piece on
Plain webhook receiver + custom controllerHand-rolled: an HTTP handler you write, calling client-go yourselfThe trigger logic is genuinely one-off and doesn't justify learning another CRD familyEvery retry, filter, and RBAC concern Argo Events already solved becomes your own team's code to maintain
CronWorkflow (Argo Workflows' own scheduler)Time-based only — fires on a schedule, no real external "event"The trigger really is just "on a schedule," and a second project isn't worth running for that aloneNot event-driven at all — can't react to a webhook, a queue message, or a file landing in a bucket

Argo Events doesn't appear as a domain on the core Kubernetes exams — CKA, CKAD and CKS never touch it — but it's explicit, named content on the CAPA blueprint's smallest domain, and it's the piece The Argo Ecosystem leans on to show how four decoupled projects become one delivery loop without ever calling each other directly.

🎬 At Mission Control
🦊

Foxy: So if I just want a Workflow to start every time someone pushes to main, why do I need three whole CRDs for that?

🐦

Pip: Because each one does exactly one job and nothing else. The EventSource catches the webhook, the bus carries it, the Sensor decides whether it counts. Split the courier from the clerk and neither has to understand the other's rules.

🐙

Olly: And I'm usually on the other end of that trigger. Pip's Sensor submits me a Workflow, I run the DAG — I never once talk back to her Sensor directly.

👺

Gizmo: Skip the whole chain, then. Have the Sensor's k8s trigger just patch the live Rollout straight to 100% the moment the webhook lands. One hop, done in a second. 😈

🤖

Recon the Robot: Do that against anything I'm reconciling and my selfHeal reverts it on the very next pass. Your Sensor's patch and my Git state disagree, and Git wins — every time.

🐢

Timmy the Turtle: Route it through Git like the rest of the loop does, Gizmo. A Sensor being fast isn't the same thing as a Sensor being a shortcut around a reconciler built specifically to undo shortcuts.

🐦

Pip: I just carry the message. What happens after I hand it off was never my job to fight about.

🐢 Timmy's checkpoint

1. Name the three core Argo Events custom resources and, in one sentence each, what each one owns. 2. What's the fundamental difference between how Argo CD decides to act and how a Sensor decides to fire a trigger? 3. You want a trigger to require two dependencies together, not either one alone. Which field do you set, and what does it look like? 4. In one sentence each: what does Argo Events create that KEDA never does, and what does KEDA change that Argo Events never touches? 5. Your EventBus pod is down. What actually happens to events arriving at an EventSource, and how would you notice? 6. Why does at-least-once delivery mean a trigger target needs to tolerate running twice for the same event? 7. Where does an unauthenticated raw webhook EventSource leave you exposed, and what's the fix?

Check your answers
  1. An EventSource listens to the outside world and publishes events it hears. An EventBus (usually NATS) carries those events as pure transport, with no filtering logic of its own. A Sensor subscribes to named events, evaluates its dependencies' filters and conditions, and fires a trigger when they're satisfied.
  2. Argo CD's controller runs a loop that never stops — polling and diffing on a timer whether or not anything changed. A Sensor runs no loop at all; it sits idle until a real event arrives from its EventBus subscription, evaluates it once, and goes back to waiting.
  3. The trigger's conditions field, a boolean expression over dependency names — e.g. "push && signed" for AND (both must fire) versus "push || signed" for OR (either alone is enough).
  4. Argo Events creates a genuinely new object — typically a Workflow — with its own identity, shaped by the specific event that caused it. KEDA never creates anything in that sense; it only changes an existing workload's replica count (or, via ScaledJob, spawns identical parameterless Jobs counted from a queue). Changing a replica count is, in turn, the one thing Argo Events never does.
  5. Every EventSource keeps listening and accepting events normally, and every Sensor object keeps looking healthy — but nothing gets published or evaluated, so no trigger ever fires. There's no propagated error status anywhere obvious; you notice by seeing nothing happen downstream and checking kubectl get eventbus -n argo-events directly.
  6. The EventBus guarantees at-least-once, not exactly-once, delivery — a pod restart or a slow acknowledgment can cause the same event to be redelivered. A trigger target that isn't safe to run twice (a payment call, a non-generateName resource creation) can be duplicated or corrupted by a redelivery the platform itself considers normal behavior.
  7. A raw webhook EventSource has no built-in signature verification — anyone who finds or guesses the URL can POST to it and fire a trigger. The fix is a Sensor data filter checking a shared secret in every payload, or using a typed source like github/gitlab that verifies a real signature before the event ever reaches a Sensor.