Bring Your Own Agent
Add a custom agent to fullsend — from harness file to CI. This guide covers the end-to-end workflow for building, registering, and dispatching custom agents on GitHub.
To configure an existing agent (model, timeout, skills, env vars) without building from scratch, see Configuring Agent Behavior. For a quick overview of all customization options, see Customizing Agents.
This guide uses the fullsend-ai/agents triage agent as a running example.
Overview
Building a custom agent takes four steps. fullsend agent new does the first one, which is most of the work:
- Generate the skeleton with
fullsend agent new. It writes every file an agent needs — how it runs, what it is allowed to do, which events start it, and what happens to its output — and registers it. See below. - Write the instructions the agent follows. This is the one file the generator cannot fill in for you, because it is the actual job.
- Test locally with
fullsend run. See Testing locally. - Commit and trigger it in CI.
The rest of this guide explains what step 0 generated, names each file, and shows how to change it. Read it when you need to go beyond the defaults, or if you would rather write everything yourself — the four steps by hand below are the long form.
Step 0: generate the skeleton
First complete Before you begin — agent new needs the fullsend CLI on your PATH and a repository you have already run fullsend github setup on. Actually running the agent afterwards needs the inference and GitHub App setup listed there too.
Then fullsend agent new writes a complete, valid, registered agent from a name and a role, so you edit prose rather than plumbing:
fullsend agent new lint-docs --fullsend-dir .fullsend \
--role triage --description "Check docs changes for broken links" ✓ Created agent "lint-docs" in .fullsend
harness/lint-docs.yaml
agents/lint-docs.md
schemas/lint-docs-result.schema.json
scripts/post-lint-docs.sh
policies/base.yaml
providers/vertex-ai.yaml
providers/github-ro.yaml
profiles/fullsend-vertex-ai.yaml
profiles/fullsend-github-ro.yaml
✓ Added agent "lint-docs"That covers steps 1, 2 and 4 of the by-hand sequence below. Fill in the marked sections of agents/lint-docs.md, then go to Testing locally.
The four steps, by hand
Building and deploying a custom agent takes four steps:
- Create the harness and agent definition — write a harness YAML file that defines how the agent runs and a Markdown file that defines what it does. See Minimum viable agent.
- Add a CEL trigger — write a trigger expression so dispatch knows when to launch your agent. See CEL Triggers Reference.
- Test locally — run your agent with
fullsend runbefore deploying to CI. See Testing locally. - Register and deploy — add your agent to
config.yamlso dispatch discovers it. See Registering your agent.
Before you begin
- fullsend CLI installed and available on your PATH.
- Repository scaffolded. Run
fullsend github setupfirst — it creates.fullsend/config.yamland the dispatch workflow. Note that a per-repo install does not vendorpolicies/,providers/orprofiles/into the repository;fullsend agent newwrites the ones your agent needs, and CI layers providers in at run time. If you are writing a harness by hand, create them yourself (see Minimum viable agent). - GCP inference provisioned (CI only). For agents running in GitHub Actions, run
fullsend inference provisionto set up Workload Identity Federation. - GitHub Apps installed (CI only). Your org needs the fullsend GitHub Apps — see Configuring GitHub.
How agents work
A fullsend agent has two parts:
- Harness file (YAML) — how the agent runs: sandbox image, policy, scripts, skills, credentials, timeouts.
- Agent definition (Markdown) — what the agent does: prompt, tools, model, skills.
Once registered, your agent runs automatically when a matching GitHub event arrives — an issue is opened, a label is applied, a comment is posted, or a PR is submitted. The harness trigger field contains a CEL expression that fullsend evaluates against incoming events to decide whether your agent should run:
GitHub event (issue opened, label added, PR comment, ...)
|
v
+-- fullsend dispatch ----------------------+
| 1. Normalize event -> NormalizedEvent |
| 2. Authorize |
| 3. Enumerate registered harnesses |
| 4. Evaluate CEL triggers |
| 5. Launch matching agents |
+-------------------------------------------+
|
v
+-- harness/my-agent.yaml ------------------+
| agent: agents/my-agent.md | <-- prompt & tools
| trigger: "event.entity.kind == ..." | <-- when to run
| policy: policies/base.yaml | <-- sandbox rules
| skills: [my-skill] | <-- domain knowledge
| pre_script: scripts/pre-... | <-- fetch data (before sandbox)
| post_script: scripts/post-... | <-- act on output (after sandbox)
+-------------------------------------------+You do not need to write a GitHub Actions workflow file for each custom agent. The dispatch workflow that fullsend github setup installs handles discovery and routing for all registered agents.
For local development and debugging, you can also run an agent directly with fullsend run my-agent — see Testing locally.
Security model: agents run inside a sandboxed environment. The sandbox policy enforces filesystem access, landlock, and process identity. Network access is typically managed via provider profiles (YAML files in a providers/ directory) referenced by name in the harness providers: list — the scaffold's shared policies/base.yaml contains no network rules, since built-in agents use providers. Custom agents can also use inline network_policies in a per-agent policy file if providers don't cover their needs. Pre-scripts run on the trusted runner before the sandbox starts; post-scripts run after it exits.
Minimum viable agent
You need a harness, an agent definition, and supporting scaffold files. fullsend agent new writes all of them for you; the layout below is what it produces, and what you need to create by hand if you are building a harness from scratch. A per-repo install does not vendor policies/, providers/ or profiles/, so a hand-written agent must supply them:
.fullsend/
+-- harness/my-agent.yaml # Execution config (you create)
+-- agents/my-agent.md # Agent prompt (you create)
+-- providers/vertex-ai.yaml # Provider definition (from scaffold)
+-- profiles/fullsend-vertex-ai.yaml # Profile definition (from scaffold; see note below)
+-- policies/base.yaml # Sandbox policy (from scaffold)harness/my-agent.yaml:
agent: agents/my-agent.md
image: ghcr.io/fullsend-ai/fullsend-sandbox:latest # Pin to a digest before CI use
policy: policies/base.yaml
providers:
- vertex-ai
openshell:
profiles:
- profiles/fullsend-vertex-ai.yaml # required — see note below
role: triage # a role your mint SERVES — not the agent's name (see note below)
slug: my-org-my-agent # install-time App discovery only; the mint never reads it
trigger: |
event.entity.kind == "work_item"
&& event.transition.kind == "label_changed"
&& event.transition.label.name == "ready-for-my-agent"
&& event.transition.label.action == "added"
timeout_minutes: 15
roleis not the agent's name. The agent's name isname:in its.md;role:selects which GitHub App and permissions the mint issues. On the default (hosted) mint,role:must be one of the built-in roles it serves —triage,coder,review,retro,prioritize,fullsend. Pick the one whose permissions fit what your agent does (a code-writing agent usesrole: coder). A made-up role likerole: my-agentreturns403from the mint. To use a new role or your own identity, you need your own mint — see Custom Agent Identity.
providers/vertex-ai.yaml — provider definition (declares a provider by name and type):
name: vertex-ai
type: fullsend-vertex-ai
credentials:
_NOOP_VERTEX_AI: ""profiles/fullsend-vertex-ai.yaml — profile definition (tells OpenShell what endpoints the fullsend-vertex-ai type grants access to). Copy this from the scaffold or fullsend-ai/agents:
id: fullsend-vertex-ai
display_name: Fullsend Vertex AI
description: Anthropic API and Google Cloud APIs for inference
category: inference
endpoints:
- host: api.anthropic.com
port: 443
protocol: rest
access: read-write
enforcement: enforce
- host: "*.googleapis.com"
port: 443
protocol: rest
access: read-write
enforcement: enforce
binaries:
- "**/claude"
- "**/claude.exe" # Claude Code 2.1.2xx runs as claude.exe, even on Linux
- "**/node"
- "**/pi"Note: A profile YAML file in
profiles/is not imported automatically by its presence alone. Only profiles listed in the harness underopenshell.profiles(or resolved via base composition) are imported. To use a custom profile, add it to your harness'sopenshell.profileslist (e.g.,profiles/fullsend-vertex-ai.yaml).
Note (CI only): the provider profile above controls network access only; real credentials are delivered via
host_files(see real-world example). Make sure you've completed the GCP prerequisites in Before you begin.
agents/my-agent.md:
---
name: my-agent
description: One-line description of what this agent does.
tools: Bash(gh,jq)
model: opus
---
You are my-agent. Your job is to [task description].
## Steps
1. Fetch input from environment variables
2. Analyze and process
3. Write JSON result to `$FULLSEND_OUTPUT_DIR/agent-result.json`
Do NOT push code, create issues, or modify anything directly.
Your only output is the JSON result file.The agent's environment also carries its budget: FULLSEND_TIMEOUT_MINUTES (the harness's timeout_minutes) and FULLSEND_ITERATION_DEADLINE (Unix seconds at which the iteration is killed). Write the result before the deadline — see fullsend run § Budget and deadline.
Network access (which APIs the agent can reach) is controlled by provider profiles or inline network_policies. The six built-in profiles (vertex-ai, github, github-ro, github-artifacts, gitleaks, package-registries) use framework-known type values (e.g. fullsend-vertex-ai, fullsend-github), but — like a fully custom provider type — still need a matching openshell.profiles entry (or one inherited via base: composition) to be imported; only the profile's type value is framework-known, not its import path. When defining a fully custom provider type, reference a remote provider definition together with a matching openshell.profiles entry (see Remote providers and profiles). For endpoints not covered by providers, inline network_policies in the policy YAML also work. Providers are the pattern used by fullsend's built-in agents, but custom agents can use whichever approach fits.
Next steps: Register your agent so dispatch discovers it, then write a CEL trigger to control when it runs. To iterate on your agent locally before registering, see Testing locally.
Real-world example: the triage agent
The fullsend-ai/agents triage agent is a full production agent. The harness below is adapted from the current harness/triage.yaml (field order adjusted for readability):
agent: agents/triage.md
doc: docs/triage.md
model: opus
image: ghcr.io/fullsend-ai/fullsend-sandbox:latest
policy: policies/triage.yaml
role: triage
slug: fullsend-ai-triage
host_files:
- src: common/env/gcp-vertex.env
dest: /sandbox/workspace/.env.d/gcp-vertex.env
expand: true
- src: ${GOOGLE_APPLICATION_CREDENTIALS}
dest: /tmp/.gcp-credentials.json
- src: ${GCP_OIDC_TOKEN_FILE}
dest: /sandbox/workspace/.gcp-oidc-token
optional: true
- src: env/triage.env
dest: /sandbox/workspace/.env.d/triage.env
expand: true
skills:
- skills/issue-labels
pre_script: scripts/pre-triage.sh
post_script: scripts/post-triage.sh
validation_loop:
script: scripts/validate-output-schema.sh
schema: schemas/triage-result.schema.json
max_iterations: 2
timeout_minutes: 10
overlays:
- when: 'runtime.forge == "github"'
pre_script: scripts/pre-triage.sh
post_script: scripts/post-triage.sh
env:
runner:
GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL}
GH_TOKEN: ${GH_TOKEN}
sandbox:
GITHUB_ISSUE_URL: "${GITHUB_ISSUE_URL}"
GH_TOKEN: "${GH_TOKEN}"Key patterns to note:
policy: policies/triage.yamlis a per-agent policy that includes filesystem, landlock, process, and network rules (via inlinenetwork_policies). This agent predates the provider-based pattern — new agents can useproviders:instead (see Minimum viable agent).host_filescopy credentials from the trusted runner into the sandbox.expand: trueresolves${VAR}references before copying.validation_loop.schemareferences the JSON schema file directly — the validation script checks agent output against it.overlaysuses CELwhenexpressions to conditionally apply scripts, skills, providers, openshell, host_files, and env vars. Resolution merges all matching entries in order: every entry whosewhenevaluates to true is applied, with later matches taking precedence over earlier ones. The CEL environment exposesevent(the triggering event),runtime.forge(the effective forge platform), andconfig(per-repo config from config.yaml). When running without an event context (e.g.,fullsend runorfullsend lock),eventis an empty map — usehas(event.source)to guard event field access:has(event.source) && event.source.system == "jira"instead of justevent.source.system == "jira"to avoid "no such key" errors.common/env/gcp-vertex.envis referenced by relative path because both files live in the same repo. If your agent lives in a different repo, reference it by URL (see Harness Field Reference — Referencing resources) or copy it locally.
For the complete list of harness fields, see the Harness Field Reference.
Agent definitions
The agent definition is Markdown with YAML frontmatter:
| Field | Purpose |
|---|---|
name | Must match the filename (sans .md) |
description | One-line summary |
tools | Allowed Bash commands (e.g., Bash(gh,jq)) |
model | LLM model |
skills | Skill names to mount |
disallowedTools | Forbidden Bash patterns |
When writing the agent body:
- The agent writes a JSON result file; scripts handle all mutations.
- Be specific — define scoring dimensions, thresholds, output schemas.
- Include decision points (branch on confidence, clarity scores, etc.).
Skills
A skill is a directory with a SKILL.md file that teaches the agent domain knowledge:
skills/issue-labels/
SKILL.md # Required: frontmatter + instructions
scripts/ # Optional: helper scripts
references/ # Optional: reference dataReference in the agent frontmatter by name (skills: [issue-labels]) and in the harness by path (skills: [skills/issue-labels]). Skills can also be URLs with integrity hashes. See Configuring with Skills for details on creating and managing skills.
For details on skill authoring, precedence, and extension points, see Configuring with Skills.
Scripts
Pre and post scripts run on the trusted runner outside the sandbox.
- Pre-scripts prepare the environment — fetch data, reset state, write files for
host_filesto copy in. - Post-scripts act on agent output — apply labels, post comments, create PRs.
Security: treat agent output as untrusted input. Validate JSON structure, validate field values against allowlists, quote all variables, and limit string lengths.
Harness composition with base
Inherit from an existing harness and override only what differs:
base: https://raw.githubusercontent.com/fullsend-ai/agents/<sha>/harness/triage.yaml#sha256=abc...
model: sonnet
slug: my-org-triage
skills:
- skills/my-enhancement
timeout_minutes: 15Base chains support up to 5 levels (MaxBaseDepth in internal/harness/compose.go). Circular references are detected and rejected. Resolution order: base chain, child overrides, overlay resolution. See the Harness Field Reference for how each field type combines.
Overlay precedence with
base:: Overlays are concatenated base-first, child-appended — the same ordering asplugins,providers, andapi_servers. BecauseResolveOverlaysmerges all matching entries in order (later matches take precedence), child overlay entries override base overlay entries with the same condition. This follows the child-overrides-base convention used by scalar and map merges.
Note:
allowed_remote_resources,allow_runtime_fetch, andmax_runtime_fetchesare NOT inherited from base harnesses — the child must declare its own. This prevents a base harness from injecting arbitrary URL prefixes or enabling runtime fetching in the child.
Org-level fallback: Separately from base-harness inheritance, the org-level
allowed_remote_resourcesfromconfig.yamlacts as a fallback for all URL resolution. URLs trusted at the org level are accepted even when the child harness omits the field. This is a distinct trust layer from base composition — the org-level list is set by organization administrators, not by base harness authors.
To configure an existing agent without building from scratch, see Configuring Agent Behavior.
Testing locally
Before registering, verify your agent works locally. Use fullsend run as a development and debugging tool — it runs your agent directly without going through dispatch:
fullsend run my-agent \
--fullsend-dir .fullsend \
--target-repo ./my-repo \
--env-file .env.localThe --env-file supplies variables your harness references (e.g. GH_TOKEN, ANTHROPIC_VERTEX_PROJECT_ID). See Running agents locally for prerequisites (GCP credentials, sandbox image) and troubleshooting.
Most agents need additional flags for credentials and target repo — see Running agents locally for the full list.
Registering your agent
Register agents in .fullsend/config.yaml so fullsend discovers them. Registration is what makes your agent visible to dispatch — without it, the agent can only be invoked via fullsend run.
Authentication for CLI commands uses GH_TOKEN, GITHUB_TOKEN, or gh auth token (in that order). For URL agents, the CLI resolves GitHub blob URLs to raw.githubusercontent.com URLs automatically.
Harness agents route via CEL triggers on arbitrary labels — there is no prefix constraint.
CLI
# Add (auto-pins URL with SHA256):
fullsend agent add \
https://github.com/fullsend-ai/agents/blob/main/harness/triage.yaml \
--fullsend-dir .fullsend
# Add local:
fullsend agent add harness/my-agent.yaml --name my-agent --fullsend-dir .fullsend
# List / update / remove:
fullsend agent list --fullsend-dir .fullsend
fullsend agent update triage <sha> --fullsend-dir .fullsend
fullsend agent remove triage --fullsend-dir .fullsendConfig file (.fullsend/config.yaml)
version: "1"
roles: [triage, coder, review]
agents:
- https://raw.githubusercontent.com/fullsend-ai/agents/<sha>/harness/triage.yaml#sha256=abc...
- name: my-cool-agent
source: harness/my-cool-agent.yaml
allowed_remote_resources:
- https://raw.githubusercontent.com/fullsend-ai/fullsend/
- https://raw.githubusercontent.com/fullsend-ai/agents/Notes:
rolescontrols which built-in agent roles are enabled. Valid values:fullsend,triage,coder,review,fix,retro,prioritize. Custom agents registered viaagents:do not need to appear in this list.- URL entries are automatically pinned with
#sha256=...byfullsend agent add. - URLs must be covered by
allowed_remote_resourcesin the same config. - On name collision, config-registered agents take precedence over built-in agents.
- Individual agents can be disabled with
enabled: false— see Disabling Agents. - Per-repo config is read from the base branch, not from PR branches.
Troubleshooting
| Symptom | Fix |
|---|---|
API Error: Error code policy_denied on the first model call (agent exits after ~2 s, 0 tokens) | The sandbox gateway denied the agent's binary, not the model. Check your profile's binaries: list has both **/claude and **/claude.exe (Claude Code 2.1.2xx runs as claude.exe). To see exactly which binary was denied: grep DENIED <run-dir>/logs/openshell-sandbox.log — see Debugging network policies locally |
| Agent crashes at 0s | Sandbox can't reach Vertex AI — verify that providers/vertex-ai.yaml is listed in your harness providers: and that ANTHROPIC_VERTEX_PROJECT_ID/CLOUD_ML_REGION are set (in your --env-file for local runs, or in the workflow env block for CI) |
unknown role "..." from agent new | The hosted mint serves five roles — see the table in agent new; for a custom role see Custom Agent Identity |
| Agent never fires, no error anywhere | The harness has no trigger:. A trigger-less agent registers and validates but is silently skipped by dispatch — fullsend agent new always writes one |
| "role field is required" | Add role: to harness |
403 / "role not allowed" from the mint | Your role: is not one the mint serves. On the hosted mint use a built-in role (triage, coder, review, retro, prioritize, fullsend); for a custom role, point FULLSEND_MINT_URL at your own mint — see Custom Agent Identity |
| Agent can't find input files | Pre-script output paths must match host_files entries |
| Provider blocks requests | Check that the required provider profile is listed in providers: and exists in the providers/ directory |
| Schema validation fails | Compare the sandbox output ($FULLSEND_OUTPUT_DIR/<result>.json) against the schema referenced in validation_loop / FULLSEND_OUTPUT_SCHEMA; re-run with --keep-sandbox to inspect |
| Agent not found | Verify registration: fullsend agent list |
| Agent not triggered by events | Verify your trigger expression — see Verifying your trigger |
allowed_remote_resources error | URL agents require a matching prefix in allowed_remote_resources — fullsend agent add sets this automatically |
fullsend run fails locally | Missing GCP credentials or sandbox image — see Running agents locally |
| Integrity hash mismatch | Remote content changed — run fullsend agent update <name> to re-pin |
See also
- Customizing Agents — overview of all customization approaches
- fullsend-ai/agents — reference implementation used throughout this guide
- Harness Field Reference — complete harness YAML field reference, merge rules, and resource referencing
- Custom Agent Identity — using a standalone mint for custom GitHub App identity
- CEL Triggers Reference — dispatch flow, NormalizedEvent fields, transition kinds, and trigger patterns
- Configuring with Skills — creating and managing skills; authoring augmentations
author-fullsend-augmentationsskill — discovery-driven guide for writing skills and sub-agents that complement shipped defaults- Configuring with AGENTS.md — repo-level instructions for all agents
- Configuring Agent Behavior — harness configuration and
base:composition - Default, derived, and custom agents — when configuration crosses into custom agent territory
- Escalation ladder — prove-it path before deriving or replacing a core agent
- Standalone mint — custom agent roles and identity
