heddle
Plugins

Plugins

Add component types, answer model calls, intercept failures, or render a run, in any language and in a process of their own.

A plugin teaches heddle something the engine does not ship: a node type, a message transform, a model provider, an error policy, a wire format, a tool. Agent Spec's own plugin system covers serialisation only: it teaches a deserialiser to read a custom component_type and says nothing about how to run it. heddle's covers both halves.

Plugins are named by whoever runs heddle, never inside a flow document. Sharing a spec can therefore never cause code to be executed. A document selects among what was already loaded; it cannot ask for anything to be loaded.

The seven kinds

What distinguishes them is not what they do but who chooses them. That decides where a plugin's name may appear, and it is the first thing to settle when you are deciding what to write.

KindChosen byAppears as
nodethe speca component_type in nodes[]
transformthe specan entry in Agent.transforms
providerthe specan llm_config, beside OpenAiConfig
componentthe specany other custom component the document names
middlewarethe operatornothing; no document may name one
storethe operatornothing; --session-store <ComponentType> selects it
encoderthe request?protocol=<name> on a server run

The first four are written into the document, so a flow's author picks them. A middleware runs on every node of every flow whether that flow asked or not, which is exactly why it is installed by whoever runs heddle rather than chosen per run. A store holds every conversation the deployment has, which is why it is the operator's too, and why only one may be installed: a process writes its conversations to one place, and there is no flag that would mean "both". An encoder renders the run for a client, and two clients hitting one flow can legitimately want different renderings, so the request picks.

Tools are not a kind. A tool is already a first-class thing with its own namespace that a spec writes into, so a plugin contributing tools needs no component type at all. See Tools.

Neither are input formats, the mirror of an encoder: where an encoder renders the run's output into another wire format, an input format reads the spec document in from one. It is chosen by the file's extension, --format, or the request's "format" field, before any component of any kind is resolved.

Neither are files, the other thing a plugin ships: whatever it declares there is copied into every node's workspace. Nothing chooses them, there is no verb behind them, and heddle reads them off the manifest without starting anything. A skill pack is a plugin with two tools and a directory.

Two ways to write one

An in-process plugin is imported into heddle and runs with the same privileges. It can read the environment, the filesystem and anything else the process can. Load one only if you would run its author's code directly.

In process. A module that default-exports a plain object. heddle imports it, so the plugin is the same program as heddle. It is the shorter path and the one the examples use, and it imports nothing: a plugin, like a flow, is something heddle reads, not something built on heddle's types.

export default {
  name: "my-plugin",
  version: "1.0.0",
  nodes: [
    {
      componentType: "RegexNode",
      createExecutor: () => ({
        execute: (input) => ({
          output: { matched: new RegExp(String(input.pattern)).test(String(input.text)) },
        }),
      }),
    },
  ],
};
heddle run flow.json --plugin ./my-plugin.js

If you want your editor to check the shape, @heddle-run/core exports definePlugin, an identity function that only adds types. Wrapping the object above in it changes nothing at run time.

Out of process. A manifest plus a program. The manifest declares what the plugin provides, as data; the program speaks JSON Lines over stdin and stdout. heddle reads the manifest to parse and compile a flow, and starts the process only when something actually calls into it.

heddle run flow.json --plugin ./my-plugin/manifest.json

--plugin decides between them by extension: a .json file is a manifest and runs out of process; anything else is imported as an ES module. Both forms are repeatable, and both are accepted by heddle validate too.

heddle-server takes the same flag, with one difference of lifetime: it loads its plugins before the port opens and one process serves every request, rather than one run. See Server.

Two properties follow from the process boundary, and neither is available in process:

  • The environment is chosen, not inherited. A plugin sees only what it is given. Credentials in heddle's own environment do not cross.
  • A spec's shape can be inspected without running its author's code. The manifest answers what the plugin provides, so heddle validate starts nobody's process.

It also means a plugin can be written in any language with a JSON parser and a read loop, the same bar a tool already clears.

What a plugin may ask heddle to do

A plugin calls back into heddle for four things, and it gets each one only by naming it. An out-of-process plugin declares them in its manifest; a host that does not grant one refuses the plugin at load, naming the capability, rather than failing the call mid-run.

CapabilityWhat it does
runToolRun one of the flow's registered tools by name
callModelAsk the model this component's spec names for one answer
emitEventPublish an event on the run's stream, under the plugin's own namespace
logSay one thing, at a level, for whoever is watching the run

There is no readFile, no fetch and no getEnv. The process boundary denies those, and re-granting them over RPC would hand back exactly what the boundary bought.

callModel is the one worth understanding before you use it. The plugin composes the request; the spec chooses the model. The call goes to the llm_config on the plugin's own component, the same field an agent carries, so a plugin ships no SDK, holds no credential, and cannot send a request anywhere the document does not say it will go.

- component_type: LlmJudge
  name: judge
  rubric: "Is the answer supported by the sources?"
  llm_config:
    component_type: OpenAiConfig
    model_id: gpt-4o-mini

Where to go next