Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Relay

Automation that reads like a spec, not a script.

Declarative workflows that chain HTTP calls, LLM reasoning, and notifications — with retries, timeouts and conditions handled for you.


The problem

Most automation starts as a 40-line script and ends as a 400-line one. The business logic — fetch this, ask the model that, alert someone if it matters — gets buried under retry loops, try/except ladders, timeout plumbing, and the one API key that has to be threaded through six function calls.

Then it breaks at 3am and nobody can read it.

Relay pulls the plumbing out. What's left is the part you actually care about:

{
  "name": "support-router",
  "steps": [
    { "id": "classify", "use": "llm",
      "with": { "system": "Reply with one word: billing, bug, urgent, or question.",
                "prompt": "${{ input.ticket }}", "effort": "low" },
      "retry": { "attempts": 2 } },

    { "id": "escalate", "use": "notify",
      "if": "steps.classify == \"urgent\"",
      "with": { "message": "Urgent ticket: ${{ input.ticket }}" } }
  ]
}

Retries, backoff, timeouts, conditional branching, and value passing are the engine's job. Yours is the twelve lines above.


Run it

Needs Node 24+. No build step — Node runs the TypeScript directly.

git clone https://github.com/ramsai676/relay.git && cd relay

node src/cli.ts explain examples/support-router.json   # show the step graph
node src/cli.ts run     examples/support-router.json   # run it

With no ANTHROPIC_API_KEY set, Relay uses a deterministic mock provider — so a fresh clone runs end to end, offline, with zero configuration. Point it at the real thing when you're ready:

export ANTHROPIC_API_KEY=sk-ant-...
npm install @anthropic-ai/sdk
node src/cli.ts run examples/lead-triage.json

Steps

use Does Key with fields
http Calls an API url, method, headers, body, expectStatus
llm Asks Claude prompt, system, effort, maxTokens, model
transform Reshapes data between steps value
filter Keeps matching items items, where
delay Waits ms
notify Posts a message message, webhook

Every step also takes if, retry, timeoutMs, and continueOnError.


Expressions

${{ ... }} reads from three roots: input (run arguments merged over the workflow's defaults), steps (any completed step's output), and env.

"${{ steps.fetch.body.items.0.email }}"    // array indexing
"Found ${{ steps.fetch.body.total }} rows" // interpolated into text
"${{ steps.fetch.body.items }}"            // a lone expression keeps its type —
                                           // this is an array, not a string

Conditions accept comparisons (==, !=, >, >=, <, <=), negation (!path), and bare truthiness. An empty array or object is falsy, so "if": "steps.search.results" reads the way you'd expect.

Inside a filter, where also sees item:

{ "id": "hot", "use": "filter",
  "with": { "items": "${{ steps.leads }}", "where": "item.score >= 75" } }

Reliability

Retries use exponential backoff — backoffMs doubles per attempt, capped at maxBackoffMs (30s default). Timeouts abort the step through an AbortSignal, so a hung fetch is actually cancelled rather than left dangling.

A failed step stops the run. Mark it continueOnError and the run proceeds with that step's output set to null, which downstream conditions can test for.

{ "id": "enrich", "use": "http",
  "with": { "url": "https://api.example.com/enrich" },
  "retry": { "attempts": 4, "backoffMs": 500, "maxBackoffMs": 8000 },
  "timeoutMs": 10000,
  "continueOnError": true }

Dry runs

--dry-run executes the full graph — conditions, expression resolution, validation — while http, llm, and notify return stand-ins instead of reaching the network. Useful for checking a workflow's shape before it costs anything.

node src/cli.ts run examples/lead-triage.json --dry-run --json

Library use

import { parse, run, MockProvider } from './src/index.ts';

const workflow = parse(await readFile('flow.json', 'utf8'), 'flow.json');

const result = await run(workflow, {
  input: { ticket: 'Checkout is down' },
  provider: new MockProvider(() => 'urgent'),   // inject anything Provider-shaped
  onEvent: (event) => console.error(event.type),
});

console.log(result.status, result.steps.at(-1)?.output);

Provider is a one-method interface, so swapping in a different model, a cached wrapper, or a recorded fixture is a constructor argument — not a fork.


Validation

Errors are collected, not thrown one at a time. A malformed workflow is fixable in a single pass:

Invalid workflow:
  - name is required and must be a non-empty string
  - steps[0].id must match ^[a-zA-Z_][a-zA-Z0-9_-]*$
  - steps[0].use must be one of: http, llm, transform, filter, delay, notify

Tests

npm test

29 tests across expression resolution, validation, and the engine — retries, skips, abort semantics, continueOnError, dry runs, and provider injection. They run offline via the mock provider.


Design notes

Zero runtime dependencies. The engine, CLI, expression language, and test suite use nothing but Node's standard library. @anthropic-ai/sdk is optional and imported lazily — it's needed only if you actually call the llm step against the real API.

Provider-abstracted. The default is claude-opus-5 with adaptive thinking and configurable effort. MockProvider makes the whole system testable without a network or a key.

JSON, not YAML. One less parser, one less dependency, and workflows stay diffable and machine-generatable.


Licence

MIT — see LICENSE.

Built by Ram Sai Kandagatla.

About

Declarative AI automation workflows — chain HTTP, LLM, filter and notify steps with retries, timeouts and conditions. Zero runtime dependencies.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages