Downcity
PluginsHooks

Guard

Explain guard blocking semantics, execution order, and why it should not be used as a value transformer

Guard

The core semantic of guard is simple:

  • if the flow is allowed, return nothing
  • if the flow is not allowed, throw

Its job is not to transform values

Many people instinctively start modifying the input inside a guard.

But the clean responsibility of a guard is only:

  • decide whether the current flow may continue

So if your main goal is "rewrite the input a bit," that belongs in pipeline.

Minimal example

await agent.plugins.guard("review.require_checked", {
  reviewed: true,
});

A plugin can define that point like this:

hooks: {
  guard: {
    "review.require_checked": [
      async ({ value }) => {
        const body = value as { reviewed?: unknown };
        if (body.reviewed !== true) {
          throw new Error("review is required before continuing");
        }
      },
    ],
  },
}

How multiple guard handlers run

If several guards attach to the same point:

  • they run in registration order
  • once one throws, the rest do not continue

So it behaves like a sequence of checkpoints.

Common good use cases

permission checks

For example:

  • does the user have a role
  • does the request have permission

preconditions

For example:

  • is a field required
  • is some prior state already ready

safety boundaries

For example:

  • must this request be explicitly confirmed first
  • must this input be reviewed before proceeding

Poor use cases

"continue, but also log something"

Split the logging into effect.

"conditionally add default values"

That belongs in pipeline.

"provide the final decision answer"

That belongs in resolve.

What happens when a plugin is disabled

If one guard plugin is currently disabled:

  • its handler is skipped
  • the other active guards still run

So the runtime meaning is:

  • active guards participate
  • disabled guards do not

Stable practices

  • make the thrown error explain clearly why the flow was blocked
  • avoid hiding lots of side effects inside guards
  • keep value transformation and blocking logic separate