Skip to content

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":{...}}}\n

Implementation 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": { }
}
FieldTypeDescription
idint64Unique id for this request. Provider echoes it back in the response.
methodstringOne of describe, configure, execute, shutdown.
paramsobjectMethod-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

FieldTypeDescription
codestringOne of the well-known codes below, or a provider-specific code.
messagestringShort human-readable message.
detailstringOptional longer detail string.

Well-known error codes:

CodeMeaning
invalid_requestThe request is malformed or cannot be decoded.
unknown_methodThe provider does not recognize method.
unknown_actionexecute called with an action not in the provider’s schema.
validation_failedAction attributes fail the provider’s runtime validation.
configure_failedconfigure failed (bad credentials, connection refused, etc.).
execute_failedexecute failed (a runtime error while running the action).
internal_errorProvider-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:

MethodParamsResultCalled
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> schema

The 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.