Provider protocol
Orchard and its providers communicate using a line-oriented JSON protocol over stdio. This page defines the wire format. If you’re writing a provider, these are the exact bytes you need to read and write.
Framing: JSON Lines
Every message is a single JSON object on its own line, terminated by \n.
Readers split on \n, parse the line as one JSON value, and match it to a
request by id.
{"id":1,"method":"describe","params":{}}\n
{"id":1,"result":{"schema":{...}}}\nImplementation notes:
- Do not pretty-print. Each frame is a single line.
- Do not emit framing bytes (length prefixes, headers). Just JSON + newline.
- Write frames atomically — if you share stdout across goroutines, serialize writes or you’ll interleave messages.
- Flush stdout after each frame. Reader on the other end is waiting for
\n. - Write logs to stderr, never stdout. stdout is reserved for the protocol.
Envelope
Orchard sends requests; the provider sends responses keyed by the same id. Responses can arrive in any order; providers that process sequentially will always respond in order.
Request
{
"id": 42,
"method": "describe",
"params": { }
}| Field | Type | Description |
|---|---|---|
id | int64 | Unique id for this request. Provider echoes it back in the response. |
method | string | One of describe, configure, execute, shutdown. |
params | object | Method-specific payload. May be omitted if empty. |
Response — success
{
"id": 42,
"result": { "schema": { "name": "echo", ... } }
}Response — error
{
"id": 42,
"error": { "code": "unknown_action", "message": "unknown action: foo" }
}A response must contain exactly one of result or error.
Error object
| Field | Type | Description |
|---|---|---|
code | string | One of the well-known codes below, or a provider-specific code. |
message | string | Short human-readable message. |
detail | string | Optional longer detail string. |
Well-known error codes:
| Code | Meaning |
|---|---|
invalid_request | The request is malformed or cannot be decoded. |
unknown_method | The provider does not recognize method. |
unknown_action | execute called with an action not in the provider’s schema. |
validation_failed | Action attributes fail the provider’s runtime validation. |
configure_failed | configure failed (bad credentials, connection refused, etc.). |
execute_failed | execute failed (a runtime error while running the action). |
internal_error | Provider-side bug or unexpected condition. |
Providers are free to define additional codes. Orchard reasons about the above codes directly; others are treated as generic failures.
Methods
See execution model for the full lifecycle. In brief:
| Method | Params | Result | Called |
|---|---|---|---|
describe | {} | { "schema": ProviderSchema } | Once, at startup, before anything else. |
configure | { "config": { ... } } | {} | Once, in run mode only, before any execute. |
execute | { "action": "name", "attrs": { ... }, "undo": { ... }? } | { "outputs": { ... }, "undo": { ... }? } | Once per action in the scenario, and once per undo during teardown. |
shutdown | {} | {} | Once, at the end of the scenario. |
Managed teardown
A provider can tell Orchard how to undo an action, so the action needs no
lifecycle { teardown { } } block. When Orchard will record an undo (the action
has no teardown block and doesn’t set managed_teardown = false), the
execute params carry undo:
{"action": "app_create_user", "attrs": {"email": "a@example.com"}, "undo": {}}undo.requested is true when the action sets managed_teardown = true,
asking for an undo even where the provider wouldn’t give one by default. A
provider that can reverse the action returns the call that does it, naming one
of its own action types:
{"outputs": {"id": "u-1"}, "undo": {"action": "app_delete_user", "attrs": {"id": "u-1"}}}Orchard checks the undo’s attributes against that action’s schema and fails
the action if they don’t fit. It stores the call in the run record, and
orchard teardown sends it as an ordinary execute. Leave undo out of the
result for no automatic teardown. Providers that predate this ignore the
params field and never return one.
Protocol versioning
Every provider schema must declare "protocol": "1". Orchard checks this at
handshake time and refuses providers that advertise a mismatched version. There
is no negotiation; the version is either right or wrong.
ProtocolVersion is defined in pkg/protocol/schema.go. When it changes,
Orchard and providers must be upgraded together.
The schema subcommand
For orchard plan, Orchard does not start the full RPC loop. Instead it runs:
<binary> schemaThe binary must print the ProviderSchema as JSON to stdout and exit 0. Any
stderr output is surfaced to the user on failure. Implement this as a separate
code path from the main loop:
func main() {
if len(os.Args) > 1 && os.Args[1] == "schema" {
writeSchemaJSON(os.Stdout)
return
}
runRPCLoop()
}Without a schema subcommand, orchard plan cannot validate your provider’s
scenarios.
Reference implementation
pkg/protocol/ in the Orchard repo defines the envelope, methods, and schema
types. Go providers can import it directly. Providers in other languages
implement the protocol from scratch against the types documented here.