Skip to main content

BEHAVIOR

A behavior type is a TypeScript module that automates resource lifecycle operations. It declares arguments, output specifications, and methods. Each method receives a context object with access to data repositories, vault secrets, and output writers. The file lives in extensions/behaviors/ and exports a behavior object.

For the YAML definition layer that configures behavior instances, see Behavior Definitions.

Export Shape

import { z } from "npm:zod@4";

export const behavior = {
  type: "@mycollective/my-behavior",
  version: "2026.05.14.1",

  globalArguments: z.object({
    region: z.string().describe("Target region"),
  }),

  resources: {
    instance: {
      description: "Managed instance",
      schema: z.object({
        id: z.string(),
        status: z.string(),
      }),
      lifetime: "infinite",
      garbageCollection: 3,
    },
  },

  methods: {
    create: {
      description: "Create a new instance",
      arguments: z.object({
        name: z.string(),
      }),
      async execute(args, context) {
        // implementation
      },
    },
  },
};

Top-Level Fields

Field Type Required Description
type string Yes Unique identifier in @collective/name format.
version string Yes CalVer format: YYYY.MM.DD.MICRO.
globalArguments z.ZodTypeAny No Zod schema for arguments shared across all methods.
resources Record<string, ResourceOutputSpec> No Named resource output specifications.
files Record<string, FileOutputSpec> No Named file output specifications.
methods Record<string, MethodDefinition> Yes Named methods the behavior exposes.
checks Record<string, CheckDefinition> No Pre-flight checks that run before methods.
reports string[] No Report names to run by default after method execution.
upgrades VersionUpgrade[] No Version migration chain for schema changes.

MethodDefinition

Each method defines its arguments and an execute function.

Field Type Required Description
description string Yes What the method does.
kind "create" | "read" | "update" | "delete" | "list" | "action" No Semantic intent of the method.
arguments z.ZodTypeAny Yes Zod schema for method-specific arguments.
execute (args, context) => Promise<MethodResult> Yes Implementation function.

MethodContext

The context parameter passed to every execute call. The most commonly used fields are listed below.

Identity

Field Type Description
modelType string The behavior's type identifier.
modelId string Instance ID of the behavior definition.
methodName string Name of the currently executing method.
globalArgs Record<string, unknown> Resolved global arguments.
repoDir string Absolute path to the repository root.
signal AbortSignal Cancellation signal.

Definition Metadata

Field Type Description
definition { id, name, version, tags } Behavior definition metadata.

Data Operations

Field Signature Description
writeResource (specName, name, data, overrides?) => Promise<DataHandle> Write a resource — validates against schema, returns a handle.
readResource (instanceName, version?) => Promise<Record<string, unknown> | null> Read a previously stored resource by instance name.
readModelData (modelName, specName?) => Promise<DataRecord[]> Read the latest version of another behavior's data by name.
queryData (predicate, select?) => Promise<DataRecord[] | unknown[]> Query data artifacts across all behaviors using a CEL predicate.
createFileWriter (specName, name, overrides?) => DataWriter Create a file writer for binary or streaming content.

readModelData returns the latest version of each matching data item, or an empty array when the behavior has no data or does not exist. Each DataRecord carries the parsed JSON in attributes, plus name, version, specName, modelId, modelType and tags.

const records = await context.readModelData!("my-vpc", "state");
const vpc = records[0]?.attributes as { VpcId: string } | undefined;

queryData uses the same predicate language as swamp data query and returns latest versions only, unless the predicate references version or isLatest. Without select it returns DataRecord[]; with select it returns the projected values.

const subnets = await context.queryData!(
  'modelType == "@myorg/subnet" && attributes.vpcId == "vpc-123"',
);

Behavior Invocation

Field Signature Description
runModel (options: RunModelOptions) => Promise<RunModelResult> Invoke another behavior's method from within the current execution.

RunModelOptions takes one of two forms:

// An existing definition, by name
{ definition: string; method: string; arguments?: Record<string, unknown> }

// Direct type execution — auto-creates the definition if it does not exist
{
  modelType: string;
  name: string;
  method: string;
  arguments?: Record<string, unknown>;
}

In the direct type form, name is the definition name. When no definition with that name exists, one is auto-created in .swamp/auto-definitions/. When one exists with a different behavior type, the invocation fails.

arguments is split between the target's global arguments and method arguments by their schemas. Method arguments win on ambiguous keys; unknown keys fail the invocation.

RunModelResult is a discriminated union on ok:

// Success
{ ok: true, resources: DataHandle[] }

// Failure
{ ok: false, error: { message: string; stack?: string } }

runModel reports failure in the returned value and does not throw. Check ok on every result:

const result = await context.runModel!({
  definition: "my-vpc",
  method: "read",
});

const created = await context.runModel!({
  modelType: "@myorg/vpc",
  name: "my-new-vpc",
  method: "create",
  arguments: { cidrBlock: "10.0.0.0/16" },
});

for (const r of [result, created]) {
  if (!r.ok) throw new Error(r.error.message);
}

The caller's extension must declare the target behavior's extension in its manifest dependencies — see the manifest reference. Invocations from extensions not listed in dependencies are rejected at runtime. Same-extension calls, built-in behavior types and user-authored behavior types need no declaration.

Safety limits apply to all runModel calls within a single top-level execution: maximum call depth of 10, cycle detection (a behavior cannot invoke itself directly or transitively), and a ceiling of 100 total invocations. Vault secrets are isolated per extension — the invoked behavior cannot access the caller's vault bindings.

Data written by the invoked behavior is owned by the target behavior's definition, not the caller's. The returned DataHandle array references the target's data.

Availability by Context

API Local method Remote worker Checks
readModelData Yes Yes No
queryData Yes Yes — select is rejected; errors if it matches access-control or infrastructure behavior data No
runModel Yes No — returns { ok: false } No

On a remote worker, runModel returns a failure result whose error.message reads:

context.runModel() is not available in remote execution — move the runModel call to a local orchestrator model or workflow.

To orchestrate behaviors on workers, chain behavior steps in a workflow. See Remote Execution for the capability proxying model.

Extension Files

Field Signature Description
extensionFile (relPath: string) => string Resolve a path relative to the extension root.

Services

Field Type Description
logger Logger Structured logger.
dataRepository UnifiedDataRepository Low-level data access.
vaultService VaultService (optional) Access to vault secrets.

dataRepository Methods

The dataRepository field on MethodContext provides low-level data access. Most methods should use writeResource, readResource, and queryData instead — dataRepository is for operations those helpers don't cover.

Author-facing methods:

Method Returns Description
getContent(type, modelId, dataName, ver?) Uint8Array | null Get raw content bytes.
findByName(type, modelId, dataName, ver?) Data | null Get data metadata (tags, version, etc).
findAllForModel(type, modelId) Data[] List all data for a behavior instance.
delete(type, modelId, dataName, ver?) void Delete a data instance — all versions, or just one.

Pass context.modelType and context.modelId for the first two parameters when operating on the current behavior's own data.

await context.dataRepository.delete(
  context.modelType,
  context.modelId,
  "error-target-a",
);

Runtime-internal methods (save, append, rename, removeLatestMarker, collectGarbage) are used by the framework to manage data lifecycle. They are not intended for direct use in behavior methods.


MethodResult

Returned by execute to report what the method produced.

Field Type Required Description
dataHandles DataHandle[] No Handles to resources or files written.

DataHandle

Field Type Description
name string Instance name of the output.
specName string Name of the resource or file spec.
kind "resource" | "file" Whether this is structured data or a file.
version number Version number of this output.
size number Size in bytes.
tags Record<string, string> Output tags.

ResourceOutputSpec

Declares a named resource output that methods can write to.

Field Type Required Description
description string No What this resource represents.
schema z.ZodTypeAny Yes Zod schema that validates written data.
lifetime "infinite" | "session" Yes How long the data persists.
garbageCollection number Yes Number of versions to keep.
tags Record<string, string> No Default tags for this output.
sensitiveOutput boolean No Auto-vault sensitive fields on write.
vaultName string No Vault name for sensitive field storage.

FileOutputSpec

Declares a named file output.

Field Type Required Description
description string No What this file represents.
contentType string Yes MIME type (e.g., application/gzip).
lifetime "infinite" | "session" Yes How long the data persists.
garbageCollection number Yes Number of versions to keep.
streaming boolean No Whether the file supports streaming writes.
tags Record<string, string> No Default tags for this output.

Checks

Pre-flight checks run before method execution and can prevent it.

checks: {
  credentials: {
    description: "Verify API credentials are valid",
    labels: ["auth"],
    async execute(context) {
      // validate credentials
      return { pass: true };
    },
  },
},
Field Type Required Description
description string Yes What the check verifies.
labels string[] No Tags for filtering checks.
appliesTo string[] No Method names this check applies to.
execute (context) => Promise<{ pass, errors? }> Yes Run the check. Return pass: false with errors to block execution.

Version Upgrades

Declare a migration chain for schema changes between versions.

upgrades: [
  {
    toVersion: "2026.05.14.2",
    description: "Rename 'instanceId' to 'id'",
    upgradeAttributes(old) {
      return { ...old, id: old.instanceId };
    },
  },
],
Field Type Description
toVersion string Target version (CalVer).
description string What the upgrade does.
upgradeAttributes (old: Record<string, unknown>) => Record<string, unknown> Transform stored attributes to the new schema.

Entries are ordered by toVersion, and the last entry's toVersion must equal the behavior's version. For a version bump with no schema change, add an entry with upgradeAttributes: (old) => old; without one, existing instances are never advanced to the new version.

Upgrades run before a method executes, against each instance's recorded typeVersion:

Instance typeVersion Result
Valid CalVer, behind Every entry with a toVersion later than it runs in order. The instance is saved with the new globalArguments and a typeVersion equal to the last entry.
Valid CalVer, current No entry runs.
Absent No entry runs, and typeVersion is not added. The method runs with globalArguments as written, and a warning is logged.
Not valid CalVer The method run fails.

swamp behavior validate and the check before each workflow step apply the same upgrades to an in-memory copy, which is never saved, and check the upgraded global arguments. An upgradeAttributes that throws fails the Global arguments item. See Upgrades during validation. A workflow step therefore runs the chain twice, so upgradeAttributes must be pure: return the new attributes and do nothing else.

An instance whose typeVersion is behind with no entry covering the gap is reported by swamp behavior get as stranded, and nothing migrates it. See Staleness.


Packaging

Declare the behavior in your extension's manifest:

behaviors:
  - my_behavior.ts

The file path is relative to the extensions/behaviors/ directory within the extension root. swamp extension push packs the file under models/ in the archive; see Manifest § Archive Structure.

Compatibility

The model spelling of each authoring name is still accepted:

Behavior spelling Compatibility spelling
export const behavior export const model
extensions/behaviors/ extensions/models/
Manifest key behaviors: Manifest key models:
SWAMP_BEHAVIORS_DIR SWAMP_MODELS_DIR

export const model takes the same object as export const behavior. A module exports one or the other. An export const behavior binding counts as the entry point only when its object has both type and methods; any other binding of that name is ignored. A module that exports both as definitions fails to load with:

Module exports both 'model' and 'behavior'; export only one of them. 'behavior' is another spelling of 'model'.

Swamp loads extensions/models/ and, beside it, extensions/behaviors/. When modelsDir in .swamp.yaml, SWAMP_MODELS_DIR, or SWAMP_BEHAVIORS_DIR sets the directory, only that directory is loaded. For the models: manifest key, see Manifest § Compatibility.

Swamp clients and workers older than the release that introduced the behavior spelling read only export const model, extensions/models/, and models:. swamp extension push and swamp extension quality report an extension that uses the behavior spelling with a behavior-spelling warning.

TypeScript API names keep the model spelling: context.runModel(), RunModelOptions, RunModelResult, context.readModelData(), context.modelType, context.modelId, and dataRepository.findAllForModel(). There is no behavior-spelled alias for these.


Runtime Permissions

Extension behavior methods run in-process with individually scoped Deno permissions. The following flags are granted:

Flag Scope
--allow-read Filesystem read access.
--allow-write Filesystem write access.
--allow-env Environment variable access.
--allow-run Subprocess execution.
--allow-sys System information access.
--allow-net Network access.

--allow-all and --allow-ffi are not granted.

Device Node Limitation

Deno.open() on character or block device nodes (e.g. /dev/ttyUSB0) does not work in the compiled binary. This is a Deno compiled-binary behavior, not a Swamp restriction.

Subprocess Workaround

Deno.Command spawns subprocesses for device I/O under the granted --allow-run permission.

const command = new Deno.Command("cat", {
  args: ["/dev/ttyUSB0"],
  stdout: "piped",
});
const { stdout } = await command.output();
const data = new TextDecoder().decode(stdout);
const stty = new Deno.Command("stty", {
  args: ["-F", "/dev/ttyUSB0", "9600", "cs8", "-cstopb", "-parenb"],
});
await stty.output();

To reach a device attached to a particular host, run the step on a worker deployed on that host, select it with placement labels, and use Deno.Command as above.