inject: Let Dependencies Decide Load Order, Not YAML Line Numbers
Understand PENDING, dependency tracking, and optional service probes, including why provider swaps move consumers automatically.
The rule: a plugin that needs a service declares inject. Cordis waits for readiness instead of making developers hand-write startup order.
This moves “who starts first?” from file layout into a runtime dependency relation that can be observed and maintained.
01 / Two rows whose order can change
The provider:
import { Service, type Context } from '@deepseek-ai/cordis'
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
}The consumer:
export const inject = ['greeter']
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('world'))
}Both entries can appear in any order in cordis.yml. The consumer’s apply runs only after greeter is registered.
The array form names services only; Cordis normalizes it to a “service name → null config” map. When a service supports local intercept configuration, inject can also use an object form that maps each service name to its config.
02 / PENDING means waiting, not half-started
When a dependency is absent, the Fiber remains PENDING:
applyis not called, so the plugin does not run halfway.- A pending Fiber does not keep Node’s event loop alive by itself.
- When the provider mounts, the consumer enters
LOADING; it reachesACTIVEonly if config andapplysucceed.
PENDING is not FAILED: a missing dependency waits, while a config or apply throw fails. This is one common explanation for “nothing happened”. Check the service name and current provider composition before debugging the plugin body.
03 / Dependencies remain live after boot
inject is not a one-time startup check. If a provider unloads or is hot-replaced, its dependent consumers unload too; when the provider returns, consumers load again.
This works with effect ownership: listeners, registrations, and external resources owned by the consumer are removed while the dependency is absent, instead of holding references to an unavailable service.
A composition can therefore replace a local shell provider with an E2B provider. Every plugin with inject: ['shell'] follows the new implementation without source changes.
04 / Hard and optional dependencies
A hard dependency fits a plugin that has no useful meaning without the service:
export const inject = ['tools']An optional dependency fits a plugin that can still operate without the service:
export function apply(ctx: Context) {
const greeter = ctx.get('greeter')
console.log(greeter ? greeter.greet('maybe') : 'no greeter available')
}Do not put an optional service in inject; the plugin will correctly remain PENDING when its provider is missing. ctx.get() is a runtime probe, not a dependency edge; when the provider appears later, that probe does not automatically rerun apply.
05 / Dependencies outlast bootstrap scripts
A hand-written bootstrap script has to maintain ordering, retries, and unload behavior. inject lets Fiber and the registry own those transitions. Adding, replacing, or temporarily removing a provider reuses the same readiness semantics.
In DSH, profiles can therefore recompose tools, models, sessions, and Agents without forcing every consumer to know the complete startup graph.
Where to go next
Sources: services tutorial, lifecycle tutorial, and Fiber implementation.
More Posts
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.
Events: Let Plugins Cooperate Without Knowing Who Listens
Use typed events and five dispatch modes to understand how Cordis broadcasts facts, runs listeners concurrently, or hands over decisions.
Lifecycle and Effects: Make Cordis Registrations Reversible
Follow a Fiber from PENDING to DISPOSED and see how effects, disposers, and HMR manage plugin side effects together.
Newsletter
Join the community
Subscribe to our newsletter for the latest news and updates