Published on

I built a small event-driven workflow for side projects

avatar for Jigar PatelJigar Patel
3 min read

I used to keep every automation as a standalone script. It worked—until it didn’t. When three scripts needed the same state, logs, and retry behavior, I had already paid the cost of a poor architecture.

I rebuilt the setup into a tiny event-driven architecture with two rules:

  • treat every event as an immutable input contract,
  • treat every handler as a replaceable unit.

The shape I use

This is the exact mental model I now follow:

Cron / Webhook / File change
          |
       [Event] --- schema validation ---> [Dispatcher]
          |
     ---------
     |   |   |
  Handler A Handler B Handler C
     |   |   |
   Output artifacts, logs, metrics

I keep it deliberately boring. No distributed tracing, no heavy message queue, no framework I can’t explain in five minutes. Just clear message boundaries and deterministic retries.

Concrete layout

projects/
  automations/
    dispatcher/
      parse-event.sh
      route.sh
    handlers/
      screenshot-archiver/main.sh
      note-indexer/main.sh
      notify-daily/main.sh
    schemas/
      event.json

I treat each handler as pure as possible:

  • read event payload,
  • do one task,
  • emit predictable output,
  • return non-zero on failure.

Practical checklist I run before adding a new handler

  • Do I already have an existing event for this action?
  • Does this handler map to exactly one output type?
  • Can I unit-test the schema validator with fixture JSON?
  • Is retry safe (idempotent) for this action?
  • Have I added a clear log format (ts, eventId, handler, result)?

Minimal command loop

# validate payload before dispatch
jq -e '.eventType and .payload and .source' event.json >/dev/null

# route the event to one handler
./dispatcher/route.sh event.json screenshot-archiver >/tmp/dispatch.log 2>&1

# inspect standardized result
cat /tmp/dispatch.log

I still keep the dispatcher tiny on purpose. The complexity lives in the handler logic where I can test and reason about it.

What I learned

  • Shared abstractions don’t scale unless the interface is stable.
  • Contracts are more valuable than code reuse.
  • Logs are architecture too; they are my operational memory.

Learning outcomes

  • I can add a new workflow in under 10 minutes once the event contract exists.
  • Failures are diagnosable without reading each handler source.
  • I can safely rerun a handler because every action is idempotent.

What to improve next

  • Add a tiny JSON schema linter job in CI.
  • Add a dead-letter folder for failed events and auto-retry policy.
  • Write a short HANDLERS.md with owner and expected output format for each handler.