Skip to main content

Authoring Workflow Bundles (pkg/workflowsdk)

The Workflow SDK (go-agent-harness/pkg/workflowsdk) is the only client-facing public package of the harness module intended for use in workflow bundles. It is a thin client library you link into a small standalone binary — called a workflow bundle — so that harnessd can discover, compile, and drive it at runtime.

A workflow bundle lets you express multi-step agent logic in plain Go: call sub-agents, emit progress phases, log messages, surface structured findings, ask questions, and invoke other registered workflows. The harness host handles process management, event streaming, and HTTP routing; your binary communicates with it over a JSONL protocol on stdin/stdout.

Bundles are out-of-process, not in-process

The "library" pattern here is not embedding the harness engine in your own binary. You write a small package main that imports only go-agent-harness/pkg/workflowsdk, and harnessd compiles and spawns it as a subprocess. All of internal/ — the engine, server, harness runner, and everything else — is inaccessible to external Go modules.

No published versioned releases

There is no published module release on a public registry. The go-agent-harness module name carries no v2+ version suffix and is not distributed via go get. Bundles are built by the harness using a replace directive that points at the harness source root on your local disk (see Discovery and build).


The client-facing public package

Every exported symbol lives in a single file: pkg/workflowsdk/sdk.go. The import path and typical alias are:

import sdk "go-agent-harness/pkg/workflowsdk"

The sole entry point is sdk.Main:

func Main(fn Run)

Main bootstraps the JSONL connection to the host, reads the initial start message (which carries args), constructs a *Context, calls fn, and writes either a result or error terminal message. If the bootstrap fails — for example, because the binary was launched without the expected stdin pipe — it writes a JSON error to stdout and calls os.Exit(1).

The Run type is just a function signature:

type Run func(ctx *Context) (any, error)

Return any JSON-serializable value as the workflow result, or return a non-nil error to fail the run.


Bundle layout

A bundle is a directory that contains exactly two required files.

workflow.json

The manifest tells the harness how to identify and build your bundle.

FieldRequiredDescription
nameyesKebab-case identifier. Must match ^[a-z0-9]+(-[a-z0-9]+)*$.
descriptionyesNon-empty human-readable description.
versionyesMust be 1.
languageyesMust be "go".
entrypointyesFilename of the Go source, typically "main.go".
when_to_usenoUsage hint shown to agents when selecting workflows.
args_schemanoJSON Schema-like object describing accepted arguments.
skillnoAssociates the bundle with a named skill.
timeout_secondsnoPer-run timeout. Default: 300 seconds (5 minutes).

Example (ux-feedback-check/workflow.json):

{
"name": "ux-feedback-check",
"description": "Workflow UX smoke that emits phase, log, finding feedback, and returns args.",
"version": 1,
"language": "go",
"entrypoint": "main.go",
"when_to_use": "Use to validate workflow discovery, run, feedback, SSE, and result handling.",
"timeout_seconds": 30
}

main.go

A package main file that calls sdk.Main with your workflow function. The example below is the real ux-feedback-check bundle from .go-harness/workflows/ux-feedback-check/main.go:

package main

import sdk "go-agent-harness/pkg/workflowsdk"

func main() {
sdk.Main(func(ctx *sdk.Context) (any, error) {
_ = ctx.Phase("Workflow UX")
_ = ctx.Log("workflow ux path running")
_ = ctx.Feedback("finding", "workflow feedback reached host", map[string]any{
"path": "api-and-tmux",
})
return map[string]any{"ok": true, "args": ctx.Args}, nil
})
}

ctx.Args carries whatever value was passed as args when the run was started via the HTTP API.


Discovery and build

harnessd automatically discovers, builds, and registers bundles from several directories without requiring a restart.

Discovery directories

Where harnessd looks for bundles

LocationDefault pathControlled by
Global workflows~/.go-harness/workflowsHARNESS_GLOBAL_DIR
Workspace workflows<workspace>/.go-harness/workflows
Global skills~/.go-harness/skillsHARNESS_SKILLS_DIR (absolute override) or HARNESS_GLOBAL_DIR fallback
Workspace skills<workspace>/.go-harness/skills

Skill-scoped bundles live at <skillRoot>/<skillName>/workflows/<workflowName>/.

To place a bundle where harnessd will find it, drop the directory into any of these paths. The hot-reload watcher polls every 5 seconds by default (HARNESS_WATCH_INTERVAL_SECONDS).

Build mechanics

When the harness builds a bundle it:

  1. Copies the bundle to a staging area

    The bundle directory is copied to a temporary location to isolate the build.

  2. Writes a synthetic go.mod with a replace directive

    The generated module file sets module workflow.local/<name>, requires go-agent-harness v0.0.0, and adds a replace go-agent-harness => <moduleRoot> directive. This is what makes go-agent-harness/pkg/workflowsdk resolvable without a published release.

  3. Runs go build

    The harness runs go build -o <binary> . with GOWORK=off and a minimal environment (only HOME and PATH). This ensures reproducible builds independent of the developer's module workspace.

  4. Caches the binary by content hash

    Binaries are cached at {cacheDir}/bin/{name}-{hash16}, where hash16 is the first 16 hex characters of a SHA-256 hash of the bundle directory. A cached binary is reused as long as the hash matches. The cache directory defaults to <workspace>/.harness/workflow-cache and can be overridden with HARNESS_GO_WORKFLOW_CACHE_DIR. The cache dir must be an absolute path. With the default relative path the binary is written relative to the harness process's working directory; if that differs from the directory at exec time, the run fails with fork/exec .harness/workflow-cache/bin/...: no such file or directory. Always set HARNESS_GO_WORKFLOW_CACHE_DIR to an absolute path (e.g. /tmp/harness-workflow-cache) when running bundles.

HARNESS_SOURCE_ROOT resolution

The build's replace directive must point at the on-disk harness module root. The harness resolves it in this priority order:

  1. HARNESS_SOURCE_ROOT environment variable — if set, this path is used directly.
  2. CWD walk — the harness walks up from the current working directory looking for a go.mod file containing module go-agent-harness.

In practice, when you run harnessd from inside the cloned repository, the CWD walk finds the root automatically. Set HARNESS_SOURCE_ROOT explicitly in CI or when starting harnessd from an unrelated directory.


SDK API and limits

Context methods

The *sdk.Context passed to your Run function exposes six methods:

func (c *Context) Agent(prompt string, opts *AgentOpts) (*AgentResult, error)

Spawns a sub-agent with the given prompt. Blocks until the agent completes and the host returns a result. Pass nil for opts to use all defaults.

AgentOpts controls how the sub-agent is configured:

type AgentOpts struct {
Label string `json:"label,omitempty"`
Phase string `json:"phase,omitempty"`
Schema any `json:"schema,omitempty"`
Model string `json:"model,omitempty"`
Provider string `json:"provider,omitempty"`
Profile string `json:"profile,omitempty"`
AllowedTools []string `json:"allowed_tools,omitempty"`
Isolation string `json:"isolation,omitempty"`
CleanupPolicy string `json:"cleanup_policy,omitempty"`
AgentType string `json:"agent_type,omitempty"`
MaxSteps int `json:"max_steps,omitempty"`
MaxCostUSD float64 `json:"max_cost_usd,omitempty"`
}

AgentResult carries the agent's output:

type AgentResult struct {
Output string `json:"output"`
Schema any `json:"schema,omitempty"`
Error string `json:"error,omitempty"`
}

What the SDK does NOT expose

Parallel, Pipeline, and Budget are host-only

The Parallel, Pipeline, and Budget primitives — along with the full internal Context that powers them — are defined in internal/workflow/ and are not accessible from a workflow bundle. If you need parallel fan-out in your bundle, implement it manually with goroutines and sync.WaitGroup, or decompose the work into separate named workflows that you invoke with ctx.Workflow().

The following packages are in internal/ and cannot be imported:

  • go-agent-harness/internal/workflow — engine, Script type, Parallel, Pipeline, budget, event constants, SourceManager
  • go-agent-harness/internal/harness — runner, tool registry, conversation store
  • go-agent-harness/internal/server — HTTP server and routes

The JSONL protocol (reference)

You will rarely need to think about this layer, but it is useful background for debugging.

When harnessd spawns your binary, communication happens over newline-delimited JSON on stdin/stdout:

DirectionMessage formatPurpose
host → child{"type":"start","result":<argsJSON>}Boots the workflow; delivers ctx.Args
child → host{"id":"req_N","type":"<op>","args":{...}}RPC call for agent, phase, log, feedback, workflow, or question
host → child{"id":"req_N","result":<json>}RPC reply
host → child{"id":"req_N","error":"..."}RPC error reply
child → host{"type":"result","result":<json>}Terminal success message
child → host{"type":"error","error":"..."}Terminal failure message

Maximum protocol message size: 1 MB. Maximum stderr captured from the child process: 32 KB. Any message sent after a terminal result message causes the host to treat the run as an error.


Putting it together: a complete bundle

Here is a worked example of a bundle that uses most SDK methods. Create a directory, add the two files, and place the directory in ~/.go-harness/workflows/ or <workspace>/.go-harness/workflows/.

{
"name": "code-review-summary",
"description": "Run a code review agent on a file, emit findings, and return a summary.",
"version": 1,
"language": "go",
"entrypoint": "main.go",
"when_to_use": "Use when you want an automated code review with structured finding events.",
"timeout_seconds": 120
}

Once the file is saved, harnessd picks it up within the next watch interval (default 5 seconds). You can then start a run via the HTTP API:

curl -s -X POST http://localhost:8080/v1/script-workflows/code-review-summary/runs \
-H "Content-Type: application/json" \
-d '{"args": {"file": "internal/server/http.go"}}' | jq .

And stream its events:

curl -s http://localhost:8080/v1/script-workflow-runs/<run_id>/events

Relevant environment variables

VariableDefaultPurpose
HARNESS_SOURCE_ROOTCWD walkPath to the harness module root used in the bundle's replace directive.
HARNESS_GO_WORKFLOW_CACHE_DIR<workspace>/.harness/workflow-cacheWhere compiled workflow binaries are cached. Must be an absolute path — with the default relative value, exec fails at runtime because the binary is written relative to the harness process's working directory. Set to an absolute path such as /tmp/harness-workflow-cache.
HARNESS_GLOBAL_DIR~/.go-harnessRoot of the global discovery directories.
HARNESS_WATCH_INTERVAL_SECONDS5How often the hot-reload watcher polls for new or changed bundles.

Next steps

  • HTTP API — see the Script Workflow HTTP routes in the server reference for the full request/response shapes for /v1/script-workflows and /v1/script-workflow-runs.
  • Event model — the events emitted during a workflow run (workflow.phase.started, workflow.finding, etc.) are described in The Event Model.
  • Configurationharnessd startup, provider selection, and workspace layout are covered in Configuration.