Skip to content

Pipeline

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.

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:

  1. Send — the Pipeline receives the passable (an interaction, event args, etc.)
  2. Through — it resolves each middleware class from the DI container and chains them together
  3. 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 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().

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.

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])

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
})

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 middleware

This is particularly useful when middleware transforms or decorates the context and you want to use the result directly.

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 pass
await 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.

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();
}
}

While the framework manages pipelines for you in the vast majority of cases, there are scenarios where reaching for it directly makes sense:

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);
});
}

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.