Skip to main content

What we’re building

We will build a workflow that triages a support ticket. Rules we write decide what is urgent, so those answers never depend on a model. For an ordinary ticket, a model drafts the first reply. If the model fails or is not there, a plain rule sends a template reply instead. We call the first kind a deterministic step: same input, same output. The second is an AI step. Putting both in one graph lets us keep a model on a short leash: it only gets the work that needs judgement.
1

Save the workflow

Create triage.json:
triage.json
Three things carry the idea:
  • check is a VALIDATE node. A ticket without a subject and a body leaves through its invalid port.
  • route is a BRANCH node. Each key in cases is a yes/no condition, and the value is the port the message leaves through. Anything else goes to defaultPort.
  • draft is an LLM_CALL node. With failureMode set to CONTINUE and routeByStatus on, a failed call does not fail the run: the message leaves through failure instead of success.
Send it to the server:
You should see:
Stuck? A 400 that mentions LLM_CALL or VALIDATE as an unknown type means this server was built without that plugin. curl http://localhost:9858/api/plugins lists the types it has.
2

Add a small helper

We will start three runs, so we put the repeated part in a shell function. It starts a run with the ticket you pass, waits until the run is no longer RUNNING, then prints the result and the status of every node.
With an API key on the server, add -H "X-API-Key: <your key>" to the three curl calls.
3

Send an urgent ticket: only rules run

You should see the page action, decided by a rule. Look at the second line: draft was skipped, so no model was called and no tokens were spent.
Stuck? If the helper hangs, jq is missing or the server needs a key: run the curl start call by hand and read its answer.
4

Send an invalid ticket: the check stops it

This ticket has no body:
You should see the ticket arrive unchanged at the rejected terminal, and every later node skipped:
The run itself is SUCCESS: setting a bad ticket aside is something the workflow was built to do.
5

Send a normal ticket: the AI step runs

What you see depends on whether draft could reach a model, and both answers are correct. We called Ollama with llama3.2, which is the default provider and model in the file.
With a local Ollama that has llama3.2 (ollama pull llama3.2), draft succeeds and the model’s text is in reply. Your wording will differ:
Either way the run ends SUCCESS with an answer the customer can be sent. decidedBy records who made the decision, so you can always tell a model’s answer from a rule’s.
Stuck? If the run itself ends FAILURE, the draft node failed before the call was made, for example because it is configured with a key that is not set (decrypted:<key> reads the environment variable YUKTA_SECRET_<KEY>). curl http://localhost:9858/api/executions/$EXEC names the failed node.
To use a hosted model, change only the draft node: provider to openai or anthropic, the model, and "apiKey": "decrypted:<name>" with the secret set as YUKTA_SECRET_<NAME> on the server. A key must never be written into the workflow itself.

What you just did

We put two kinds of work in one graph. Rules decided what is urgent and what is invalid, the same way every time. A model got only the step that needs wording, and we gave it a way out, so a missing or failing model costs us a template reply and not a failed run. Every answer says whether a rule or a model made it.

Where to go next

Let an assistant build a workflow

Next tutorial: have Claude Code write and run a workflow like this one.

Why one graph holds both

Where AI belongs in a workflow, and where it does not.

Call an LLM from a workflow

Providers, secrets, retries and the options of LLM_CALL.

Set token budgets

Cap what a model may spend in one run.