We use cookies

We use cookies to ensure you get the best experience on our website. For more information on how we use cookies, please see our cookie policy.

By clicking "Accept", you agree to our use of cookies.
Learn more.

GuideMiddleware & Dependency Injection

Middleware & Dependency Injection

Middleware lets you run logic before and after every task on a client, without touching individual task definitions. Common uses include injecting request IDs, enriching inputs with shared data, encrypting/decrypting payloads, and normalizing or augmenting outputs.

⚠️

This feature is experimental, and middleware hook signatures may change in future releases.

Hatchet’s Python SDK uses FastAPI-style dependency injection to run logic before tasks and inject the results as parameters. Dependencies are declared as functions and wired into tasks with Depends.

Defining Middleware

Define your dependency functions — they receive the workflow input and context, and their return values are injected into the task as parameters.

How Middleware Executes

Dependencies are resolved before each task execution. Each dependency function receives the original workflow input and the task context, and its return value is injected as a named parameter to the task function.

Using Middleware in Tasks

Inject dependencies into your tasks using Depends and type annotations. The dependency results are passed directly as function parameters.

⚠️

Your dependency functions must take two positional arguments: the workflow input and the Context (the same as any other task).

Running a Worker

No special worker configuration is needed — dependencies are evaluated automatically each time a task runs.

Practical Examples

The examples below show TypeScript middleware for common production patterns. Each can be adapted to the Python dependency injection model by extracting the same logic into a dependency function.

End-to-End Encryption

Encrypt sensitive input fields before they reach the Hatchet server, and decrypt the output on the way back. This ensures plaintext data never leaves your worker process.

The before hook decrypts incoming data so your task function works with plaintext. The after hook encrypts the output before it is stored. The encryption key never leaves the worker environment.

Offloading Large Payloads to S3

When task inputs or outputs exceed Hatchet’s payload size limit (or you simply want to keep large blobs out of the control plane), upload them to S3 and pass a signed URL instead.

⚠️

The caller is responsible for uploading oversized inputs to S3 before triggering the task. The before hook only handles the download side. You can use the same uploadToS3 helper on the caller side to upload the input and pass { __s3Url: url } as the task input.

FAQ

What is Hatchet middleware and how does it differ from Express middleware?

Hatchet middleware runs inside the worker process around each task invocation — not on an HTTP request path. A before hook transforms input before the task runs, and an after hook transforms output after. Unlike Express middleware, there is no next() function; hooks return their result directly and the runner chains them automatically.

Can I use middleware with both tasks and workflows?

Yes. Middleware is registered on the HatchetClient instance, so it applies to every task created from that client — whether the task is a standalone client.task() or part of a multi-step client.workflow(). Each step in a workflow will have middleware applied independently.

Does middleware run on the server or on the worker?

Middleware runs entirely on the worker. The Hatchet server never sees or executes your middleware code. This is what makes patterns like end-to-end encryption possible — plaintext data stays within your infrastructure.

What happens if my middleware throws an error?

If a before or after hook throws (or returns a rejected Promise), the task run fails with that error. There is no automatic retry of middleware itself, but the task’s configured retry policy will still apply, re-running the task (and its middleware) from scratch.

Can I use async/await in middleware hooks?

Yes. Both before and after hooks can be synchronous or asynchronous. If a hook returns a Promise, the worker will await it before proceeding to the next hook or the task function.

How do I share state between before and after hooks?

The after hook receives the task input (after before hooks have run) as its third argument. Add fields in before (e.g. startedAt, traceId) and read them from input in after. There is no separate shared context object — the input itself is the carrier.

Does middleware apply to child tasks spawned via fanout?

Middleware is scoped to the client instance. If a child task is defined on the same middleware-enabled client, its middleware will run when that child task executes. If the child task uses a different client instance, only that client’s middleware (if any) applies.

Can I selectively skip middleware for certain tasks?

Middleware applies to all tasks on a given client. To skip middleware for specific tasks, create a second client without middleware and define those tasks on it. This is a deliberate design choice — middleware is a cross-cutting concern, and selective opt-out is handled at the client boundary.

Is there a performance overhead to using middleware?

Middleware hooks are plain JavaScript functions that run in-process on the worker. The overhead is the execution time of your hook code. For lightweight operations (adding a field, logging), the overhead is negligible. For heavier operations (network calls like S3 uploads or decryption), the task’s total duration will include that time, so keep hooks as efficient as possible.

What is the difference between global types and middleware types in TypeScript?

Global types (HatchetClient.init<GlobalInput, GlobalOutput>()) define fields that callers must provide when triggering a task. Middleware types (inferred from withMiddleware return values) define fields that are injected at runtime by the worker. Both end up on the task’s input type, but only global types appear in the caller-facing run() signature.

Can I use middleware for rate limiting or authentication?

Yes. A before hook can check rate limits, validate API keys, or verify JWTs before the task runs. If the check fails, throw an error to abort the task. However, for rate limiting specifically, consider using Hatchet’s built-in rate limiting feature, which operates at the scheduling layer and is more efficient than in-worker checks.

How do I test middleware in isolation?

Middleware hooks are plain functions — you can unit-test them directly by calling them with mock input and a mock context object. For integration tests, the e2e test pattern of creating a client, attaching middleware, defining a task, starting a worker, and asserting on the result works well. See the middleware example on GitHub for a complete test setup.