waterfall: Intercept, Wrap, and Short-Circuit with next()
Follow a Cordis waterfall chain to see listeners delegate downstream, wrap results, or short-circuit when they own the decision.
Remember one rule: an observer must call next(); only a listener that owns the decision should omit it and return directly.
waterfall is not a broadcast. Each listener receives the arguments and a continuation. It can delegate to the default behavior or wrap the result that comes back from downstream.
01 / The chain is nested functions
import type { Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
}
}
export async function apply(ctx: Context) {
ctx.on('demo/transform', async (input, next) => {
const downstream = await next()
return downstream.toUpperCase()
})
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **'
return next()
})
const input = 'hello'
const result = await ctx.waterfall('demo/transform', input, async () => input)
console.log(result)
}The innermost function is the default behavior. Listeners wrap around it in registration order, and next() is what moves execution downstream.
02 / Wrapping and short-circuiting are different intents
- Delegate: the listener does not own the result, so it calls
next(). - Wrap: it awaits
next()and transforms the result, such as uppercasing or adding diagnostics. - Short-circuit: it owns the decision, skips
next(), and returns a replacement.
In the example, hello reaches the default behavior and becomes HELLO; blocked words returns ** blocked ** at the policy listener, so the default function never runs.
03 / Forgetting next() silently consumes the default
ctx.on('agent/request', async (request, next) => {
console.log('observed request')
return next()
})If an observer omits next(), its return value ends the entire waterfall. A downstream model request, tool execution, or default policy may never happen without looking like an exception.
Calling or not calling next is therefore a responsibility declaration: omission means veto or replacement; calling it means delegation.
04 / Two DSH decision points
DeepSeek Harness uses waterfall for replaceable policy:
agent/requestlets a plugin replace model-request configuration or wrap it before the provider.approval/requestlets a policy answer for the user, while ordinary observers can delegate downstream.
The owning service may define its own error boundary: for example, approval/request normalizes an answerer exception into an unavailable result. That behavior is not universal to every waterfall event.
The policy does not need to be embedded in the agent loop. A plugin attaches to the event, and waterfall semantics make result ownership explicit.
05 / Ask three questions before using it
- What is the default behavior? Write it as the innermost
nextfunction. - Is this listener observing, or does it own the replacement decision? Observers must delegate.
- Should the result be wrapped? If yes, transform it after
await next()instead of bypassing downstream.
Answering these questions turns waterfall into a readable middleware chain instead of an opaque callback list.
Where to go next
- Events: Let Plugins Cooperate Without Knowing Who Listens
- Configuration: Options with Validation
- Cordis Inside DeepSeek Harness
Sources: events tutorial, waterfall semantics in the primer, and Events implementation.
More Posts
Services and Context: Share Capabilities Without Binding Implementations
Use Service, Context, and declaration merging to understand how providers, consumers, and scopes connect DeepSeek Harness capabilities.
Cordis: Turning a Running Application into a Reversible Plugin Tree
Follow cordis.yml, Loader, and Context to see how Cordis composes, replaces, and safely tears down runtime capabilities in DeepSeek Harness.
Configuration: Give Plugins Validated Options and Defaults
Define Cordis plugin configuration with Schemastery and see how defaults, path-aware errors, and FAILED Fibers prevent a sick start.
Newsletter
Join the community
Subscribe to our newsletter for the latest news and updates