Deterministic orchestration for real systems
Argyll is an open orchestration platform built for systems with callbacks, retries, delays, long-running work, and probabilistic services. It keeps execution predictable while giving you a cleaner model than hand-maintained process logic.
Instead of hard-coding and updating every path, you define the outcomes that must be satisfied, and Argyll resolves and executes the work required to reach them.

Most orchestration complexity shows up as duplicated coordination logic, retry handling, and special cases spread across services. Process definitions multiply as requirements evolve, and the cost of change keeps climbing.
Argyll moves orchestration back into an explicit execution layer, and the result is shared execution, clearer system boundaries, reliable retries, and predictable behavior in production.
What people use it for
Coordinate payment confirmation, inventory reservation, shipment, and customer communication without burying fulfillment logic across multiple services.
Complete the transaction, record it, and notify the customer with coordination handled in one execution layer.
Combine verification, screening, case creation, and notifications with one model that stays coherent as review requirements change.
Share customer and message context across channels with one common execution model.
Handle account setup, permission changes, access grants, and audit updates without scattering lifecycle logic across product and platform services.
Keep invocation, state transitions, and downstream execution under explicit control so costs and behavior do not sprawl.
Each unit of work is a Step: an HTTP endpoint, an inline script, or a reusable sub-flow, each declaring what it needs and what it produces. A Flow is one execution run. You name the Goal Steps and supply any initial values; Argyll plans and executes the rest.
A Flow advances through durable state. Results enter that state intentionally, and what runs next is derived only from what the Flow already knows. At any point you can inspect a Flow, see exactly where it is, and explain why it is doing what it is doing.
The same Flow state leads to the same behavior, side effects follow committed state, and retries resume from stable state with consistent results. That matters where real systems break: timeouts, delayed callbacks, partial completion, and repeated attempts across multiple services.
Steps share data through named attributes. When a Step's output attribute matches another Step's input, Argyll connects them. No explicit wiring required. Collection policies control how a Step handles multiple upstream providers: first, last, some, all, or none. Optional inputs can also set a Collection Deadline, so a Step can close its input window and continue with the best value available, a default, or no value.
Argyll tracks dependencies as execution progresses. When a downstream Step no longer needs more values, upstream work that only served that consumer can be skipped. Skips happen three ways: a Predicate that evaluates to false, a dependency that no longer has any consumers, or a match filter on a required input that cannot be satisfied. In all cases, upstream work that existed only to serve the skipped Step is pruned with it.
What you get
When two Goal Steps need the same upstream result, Argyll resolves it once and shares it. No coordination code, no duplicate calls, no double billing.
Each service declares its inputs and outputs. Argyll handles the wiring. Services do not call each other or need to know what else is in the Flow.
Add a new Step or adjust an existing one. Each new Flow plans against the current registry. Existing Steps and their configurations stay unchanged.
AI services may be probabilistic, but the surrounding system does not have to be. Argyll keeps invocation, state transitions, and downstream execution under explicit control.
Should you use it
- You keep rewriting the same coordination logic in different places
- Flows overlap and should share dependencies
- Requirements change frequently
- You want reliable retries and predictable recovery
- You care about inspectable execution and explicit state
- You need human approval queues and worklists
- You want a visual process modeling tool first
- You need runtime mutation of execution plans
- You are building a low-code business-user tool
- You need a scheduler more than an execution engine
See it running
Subscribe by aggregate: ["catalog"] for the step registry, ["cluster"] for per-step health across the cluster, or ["flow", "your-flow-id"] for a specific Flow. With include_state: true, the server first sends the current projected state, then streams live events.
const ws = new WebSocket("ws://localhost:8080/engine/ws");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "subscribe",
data: {
sub_id: "flow-detail",
aggregate_ids: [["flow", "hello-flow"]],
include_state: true,
},
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "subscribed") {
msg.items.forEach((item) =>
console.log("Current state:", item.id, item.data, item.sequence));
} else {
console.log("[event]", msg.type, msg.data);
}
};git clone https://github.com/kode4food/argyll.git
cd argyll
docker compose upcurl -X POST http://localhost:8080/engine/step \
-H "Content-Type: application/json" \
-d '{
"id": "hello-script",
"name": "Hello Script",
"type": "script",
"attributes": {
"name": {"role": "required", "type": "string"},
"greeting": {"role": "output", "type": "string"}
},
"script": {
"language": "lua",
"script": "
return {
greeting = \"Hello, \" .. name .. \"!\"
}
"
}
}'curl -X POST http://localhost:8080/engine/flow \
-H "Content-Type: application/json" \
-d '{
"id": "hello-flow",
"goals": ["hello-script"],
"init": {"name": ["Argyll"]}
}'curl http://localhost:8080/engine/flow/hello-flow
curl http://localhost:8080/engine/healthSee the API Reference for the full endpoint and WebSocket documentation, or the Getting Started guide for setup and local development details.
What a Step looks like
A Step declares what it needs and what it produces. Pick the simplest type that fits the job: sync when the work finishes in one request, async when something needs to continue in the background, and script when the logic belongs inside the engine.
HTTP Steps can choose a method and use required inputs inside the URL. Argyll resolves placeholders before calling the endpoint, so a Step can target a clean JSON endpoint without an orchestration envelope. POST and PUT Steps receive input arguments as a JSON body; GET Steps resolve required inputs into the URL. In both cases, Flow ID, Step ID, receipt token, and async webhook URL travel in Argyll headers. Successful responses return output arguments directly; failures use HTTP status codes and Problem Details.
A Step can also include a predicate. If the predicate evaluates to false, Argyll marks the Step skipped and makes no service call.
Use this when a service can return its result immediately.
{
"id": "lookup-customer",
"name": "Lookup Customer",
"type": "sync",
"http": {
"method": "GET",
"endpoint": "https://api.example.com/customers/{customer_id}",
"timeout": 5000
},
"attributes": {
"customer_id": { "role": "required" },
"email": { "role": "output" },
"phone": { "role": "output" }
}
}Use this when work starts now and completes later by webhook.
{
"id": "process-payment",
"name": "Process Payment",
"type": "async",
"http": {
"method": "POST",
"endpoint": "https://api.example.com/payments/{payment_id}/capture",
"timeout": 1000
},
"attributes": {
"payment_id": { "role": "required" },
"amount": { "role": "required" },
"currency": { "role": "required" },
"transaction_id": { "role": "output" }
}
}Use this for small in-engine transforms, routing, and glue logic.
{
"id": "calculate-discount",
"name": "Calculate Discount",
"type": "script",
"script": {
"language": "lua",
"script": "
local amt = amount * (1 - discount_percent)
return { discounted_amount = amt }
"
},
"attributes": {
"amount": { "role": "required" },
"discount_percent": { "role": "required" },
"discounted_amount": { "role": "output" }
}
}Use this when a Step should run only when the current Flow state satisfies a simple condition.
{
"id": "send-discount",
"name": "Send Discount",
"type": "sync",
"predicate": {
"language": "jpath",
"script": "$.customer.tier == \"premium\""
},
"http": {
"endpoint": "https://api.example.com/discounts/send",
"timeout": 5000
},
"attributes": {
"customer": { "role": "required" },
"sent": { "role": "output" }
}
}Use this when an input can accept multiple upstream providers or wait only until a Collection Deadline.
{
"id": "choose-profile",
"name": "Choose Profile",
"type": "sync",
"http": {
"endpoint": "https://api.example.com/profile/render",
"timeout": 5000
},
"attributes": {
"profile": {
"role": "optional",
"type": "object",
"optional": {
"collect": "last",
"deadline": 2000,
"default": "{}"
}
},
"rendered": { "role": "output" }
}
}Use this when you want to package a reusable set of goals behind one Step.
{
"id": "authorize-user",
"name": "Authorize User",
"type": "flow",
"flow": {
"goals": ["check-permissions"]
},
"attributes": {
"uid": { "role": "required", "required": {
"mapping": { "name": "user_id" }
}},
"authorized": { "role": "output", "output": {
"mapping": { "name": "is_authorized" }
}}
}
}