Argyll · Plan and Perform Only What Matters

This ain't no workflow engine. Argyll is a goal-driven orchestrator. You give it goals and it works backward to achieve them. Don't need to run a step? Skip it. Need one step to process fifty different things? No problem. Have a step that won't call back for 73 years? Argyll can handle it. Set your goals and let it rip

Argyll logo
Argyll UI screenshot

Quickstart

Register a Step · Post your step spec (see example JSON below)
curl -X POST http://localhost:8080/engine/step \
  -H "Content-Type: application/json" \
  -d @step.json
Check Health · All steps and a specific step
curl http://localhost:8080/engine/health
curl http://localhost:8080/engine/health/send-email
Delete a Step · Retire it when it’s no longer needed
curl -X DELETE http://localhost:8080/engine/step/send-email
Preview a Plan · See what runs for goal "send-email"
curl -X POST http://localhost:8080/engine/plan \
  -H "Content-Type: application/json" \
  -d '{"goals":["send-email"],"state":{}}'
Start a Flow · Kick off using that goal set
curl -X POST http://localhost:8080/engine/flow \
  -H "Content-Type: application/json" \
  -d '{"id":"welcome-flow","goals":["send-email"],"state":{}}'
Inspect Flow State · Fetch current state/events
curl http://localhost:8080/engine/flow/welcome-flow

Full OpenAPI specs live in docs/api. Read more in the Argyll GitHub repository and see the README for running locally

Step Definition Example

Register steps that declare inputs/outputs. Choose one or more goal steps—Argyll plans the rest

{
  "id": "send-email",
  "type": "async",
  "http": { "url": "https://api.example.com/send-email" },
  "attributes": {
    "recipient": { "role": "required", "type": "string" },
    "body": { "role": "optional", "type": "string", "default": "\"Hello!\"" },
    "message_id": { "role": "output", "type": "string" }
  }
}

Start a flow with your goals, monitor via WebSocket at /engine/ws, and edit steps live in the UI

Live Events (WebSocket)

Subscribe to Events · Send a subscribe message after connect

Subscribe by aggregate: ["engine"] for engine events or ["flow", "your-flow-id"] for a specific flow. The server responds with current state, then streams live events

const ws = new WebSocket("ws://localhost:8080/engine/ws");

ws.onopen = () => {
  ws.send(JSON.stringify({
    type: "subscribe",
    data: { aggregate_id: ["flow", "welcome-flow"] }
    // or: { aggregate_id: ["engine"] } for engine events
  }));
};

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === "subscribed") {
    console.log("Current state:", msg.data, "sequence:", msg.sequence);
  } else {
    console.log("[event]", msg.type, msg.data);
  }
};
Subscribe Response · Current state on subscription
{
  "type": "subscribed",
  "id": ["flow", "welcome-flow"],
  "data": { "id": "welcome-flow", "status": "active", "plan": {...} },
  "sequence": 42
}
Sample Event · Flow started
{
  "type": "flow_started",
  "timestamp": 1733151600000,
  "id": ["flow", "welcome-flow"],
  "data": {
    "flow_id": "welcome-flow",
    "plan": { "goals": ["send-email"], "steps": { "send-email": {} } },
    "init": {}
  },
  "sequence": 43
}