Pipeline
Introduction
Section titled “Introduction”Every time a command runs, a button is clicked, or an event fires, a Pipeline orchestrates the middleware chain behind the scenes. You’ve already seen what middleware can do in the Middleware guide — the Pipeline is the engine that makes it all work.
In most projects, you’ll never touch the Pipeline directly. The framework creates one for every execution, sends the interaction through your middleware stack, and calls your handler’s execute() method at the end. But if you’re building a custom execution flow, an advanced plugin, or just curious about how the internals work, the Pipeline is exported from @warden/core and ready to use.
How it works
Section titled “How it works”The Pipeline was redesigned from the ground up for Warden. Unlike many pipeline implementations that carry mutable state between executions, Warden’s Pipeline is stateless — a fresh instance is created for every execution. There’s no shared state to worry about, no accidental bleed between requests, and no need to reset anything.
Each execution follows a simple flow:
- Send — the Pipeline receives the passable (an interaction, event args, etc.)
- Through — it resolves each middleware class from the DI container and chains them together
- Then — after all middleware has called
next(), the final handler runs
If any middleware short-circuits by not calling next(), the chain stops and the handler never executes. This is exactly how middleware like IgnoreBots or IsModerator prevents unwanted execution.
The chainable API
Section titled “The chainable API”The Pipeline provides a fluent, chainable API that reads like a sentence:
import {Pipeline} from '@warden/core';
await new Pipeline<CommandInteraction>() .send(interaction) .through([EnsureGuildIsAvailable, IsModerator]) .then((ctx) => command.execute(ctx, params));Each method returns the Pipeline instance, so you can chain them in any order — though .send() and .through() must come before .then() or .thenReturn().
.send(passable)
Section titled “.send(passable)”Sets the value that will be passed through the middleware chain. This is typically the interaction or event arguments:
new Pipeline<CommandInteraction>().send(interaction)new Pipeline<[Message]>().send([message])new Pipeline<ButtonInteraction>().send(buttonInteraction)The type parameter <T> on Pipeline ensures that your middleware and handler receive the correct type.
.through(middleware)
Section titled “.through(middleware)”Accepts an array of middleware classes (or nested arrays for OR composition). The Pipeline resolves each class from the DI container, so constructor injection works automatically:
.through([EnsureGuildIsAvailable, IsModerator, NotSelf]).then(handler)
Section titled “.then(handler)”The callback that runs after all middleware has passed. This is where your actual logic lives:
.then((ctx) => { // ctx is the (possibly transformed) passable // all middleware has passed at this point}).thenReturn()
Section titled “.thenReturn()”Sometimes you don’t need a final handler — you just want the passable back after middleware has had a chance to inspect or transform it. .thenReturn() does exactly that:
const ctx = await new Pipeline<CommandInteraction>() .send(interaction) .through([EnrichContext, ValidateGuild]) .thenReturn();
// ctx is the interaction, possibly enriched by middlewareThis is particularly useful when middleware transforms or decorates the context and you want to use the result directly.
AND/OR composition
Section titled “AND/OR composition”The Pipeline natively understands the same AND/OR composition you use in middleware arrays. Flat arrays mean AND (all must pass), nested arrays mean OR (at least one must pass):
// All three must passawait new Pipeline<CommandInteraction>() .send(interaction) .through([EnsureGuildIsAvailable, IsModerator, NotSelf]) .then(handler);
// EnsureGuildIsAvailable AND (IsAdmin OR IsModerator)await new Pipeline<CommandInteraction>() .send(interaction) .through([EnsureGuildIsAvailable, [IsAdmin, IsModerator]]) .then(handler);When the Pipeline encounters a nested array, it runs each middleware in the group concurrently. If at least one calls next(), the chain continues. If none do, the chain stops — just as if a single middleware had short-circuited.
Dependency injection
Section titled “Dependency injection”The Pipeline resolves middleware classes from the DI container automatically. You never instantiate middleware yourself — just pass the class reference:
// The Pipeline resolves EnsureGuildIsAvailable from the container,// which means its constructor dependencies (GuildService, etc.) are injected.through([EnsureGuildIsAvailable, IsModerator])This means your middleware can inject any registered service:
import {Middleware, type NextFn} from '@warden/core';import type {HandlerMeta} from '@warden/core';import type {CommandInteraction} from 'discord.js';
export default class EnsureGuildIsAvailable extends Middleware<CommandInteraction> { constructor(private guildService: GuildService) {} // injected by the container
public async handle(ctx: CommandInteraction, _meta: HandlerMeta, next: NextFn): Promise<void> { await this.guildService.findOrCreate(ctx.guildId!); await next(); }}Using Pipeline directly
Section titled “Using Pipeline directly”While the framework manages pipelines for you in the vast majority of cases, there are scenarios where reaching for it directly makes sense:
Custom execution flows
Section titled “Custom execution flows”If you’re building a multi-step moderation workflow — say, a review queue where reports pass through several validation stages — the Pipeline gives you a clean abstraction:
import {Pipeline} from '@warden/core';
async function processReport(report: ModerationReport): Promise<void> { await new Pipeline<ModerationReport>() .send(report) .through([ValidateReport, CheckDuplicates, EnrichWithHistory]) .then(async (ctx) => { await this.reportQueue.enqueue(ctx); });}Plugin development
Section titled “Plugin development”If you’re writing a Warden plugin that introduces its own handler type, you can use the Pipeline to run middleware before your plugin’s handlers — keeping the same developer experience as the rest of the framework.