Repository navigation
Tutorial
This walks through running the inbox-triage agent in both billing modes, writing an agent playbook of your own, and scheduling it daily. Commands assume Linux with bash.
kind: agent ships in noetl 5.1.0 and later. Install it one of these ways.
Debian / Ubuntu:
echo 'deb [trusted=yes] https://noetl.github.io/apt jammy main' | sudo tee /etc/apt/sources.list.d/noetl.list && sudo apt-get update && sudo apt-get install noetlmacOS / Linux with Homebrew:
brew tap noetl/tap && brew install noetlFrom crates.io (needs Rust):
cargo install --bins noetlWith pip (the wheel bundles the same CLI; wrapper 5.3.2 carries CLI 5.3.2):
pip install "noetl>=5.3.2"Check that you have 5.1.0 or newer:
noetl --versiongit clone https://github.com/noetl/agent-playbooks.git ~/projects/agent-playbooks && cd ~/projects/agent-playbooks-
Create a key at console.anthropic.com → API Keys. It starts with
sk-ant-api. -
Add credits under Settings → Billing. API usage is prepaid and separate from any Claude subscription; without credits every call returns
credit balance is too low. -
Store the key in a file only you can read. Open
~/.config/noetl/anthropic.keyin an editor and paste the key alone on one line:install -d -m 700 ~/.config/noetl && touch ~/.config/noetl/anthropic.key && chmod 600 ~/.config/noetl/anthropic.key
Verify the shape without printing it:
head -c 7 ~/.config/noetl/anthropic.key | grep -q '^sk-ant-' && echo ok || echo "wrong key"
-
Run:
NOETL_CREDENTIAL_ANTHROPIC="$(cat ~/.config/noetl/anthropic.key)" noetl run playbooks/inbox-triage/triage_api.yaml --runtime local --set account=work --verbose
What you should see: the agent calls list_unread, the line 🔧 Agent tool list_unread → …/tools/list_unread.yaml appears, and the step finishes after 2 turns with a list of the messages that need action. A run uses about 1,100 input and 170 output tokens.
-
Install the Claude Code CLI:
curl -fsSL https://claude.ai/install.sh | bash -
Sign in once, interactively, choosing Claude account with subscription:
~/.local/bin/claude -
Confirm headless mode works:
echo "Reply with exactly: pong" | ~/.local/bin/claude -p --output-format json | jq '{is_error, result}'
-
Run:
noetl run playbooks/inbox-triage/triage_subscription.yaml --runtime local --set account=work --set claude_bin=$HOME/.local/bin/claude --verbose
Here NoETL fetches the mail in step fetch and passes it to Claude in the prompt; Claude gets no tools. The result's framework field reads claude-code.
An agent playbook is an ordinary playbook with a kind: agent step. Tools are other playbooks.
The tool (playbooks/weather/tools/forecast.yaml): the model's input object arrives as workload.*; the last step's output is what the model sees.
apiVersion: noetl.io/v2
kind: Playbook
metadata:
name: forecast
workload:
city: ""
workflow:
- step: start
next:
- step: get
- step: get
tool:
kind: http
method: GET
url: "https://wttr.in/{{ workload.city }}"
params:
format: "3"The agent (playbooks/weather/brief.yaml):
apiVersion: noetl.io/v2
kind: Playbook
metadata:
name: weather_brief
executor:
profile: local
requires:
tools: [agent]
workflow:
- step: start
next:
- step: brief
- step: brief
tool:
kind: agent
framework: claude
model: claude-sonnet-5-5
prompt: Should I take an umbrella in Berlin and in Paris today?
max_turns: 4
auth:
type: api_key
credential: anthropic
tools:
- name: forecast
description: Today's weather for one city.
input_schema:
type: object
properties:
city: { type: string }
required: [city]
playbook: tools/forecast.yamlRules of thumb:
-
Describe tools for the model.
descriptionandinput_schemaare all it sees; theplaybook:binding never reaches it. - Keep tool output small. It is sent back to the model on every later turn. Return sender/subject/snippet, not full message bodies.
-
Set
max_turnsto the calls you expect plus one or two. The step fails rather than loops past it. -
Declare
executor.requires.tools: [agent]so an oldernoetlrefuses the playbook with a clear message instead of skipping the step.
The same agent on a subscription: switch framework: claude-code, delete auth: and tools:, and either fetch the data in earlier steps (as triage_subscription.yaml does) or grant Claude Code tools with allowed_tools / mcp_config. See Configuration Reference.
playbooks/inbox-triage uses stub mail so it runs anywhere. To read your real Gmail accounts, follow Connect Gmail:
- a one-time Google OAuth client;
-
noetl credential google gmail-<account> …once per account; -
playbooks/gmail-triage/triage.yaml.
Each Gmail call there is an http step with auth: {type: google_oauth, credential: gmail-<account>}.
mkdir -p ~/.config/systemd/user && cp systemd/inbox-triage.* ~/.config/systemd/user/systemctl --user daemon-reload && systemctl --user enable --now inbox-triage.timer- The timer fires at 07:30;
Persistent=trueruns a missed job at the next start. - The service reads the key file at run time; the unit never contains the key.
- To use the subscription instead, swap the commented
ExecStartline ininbox-triage.service. - Results:
journalctl --user -u inbox-triage.service. Next run:systemctl --user list-timers. - To run while you're logged out:
sudo loginctl enable-linger $USER.
Next: Architecture explains what happens inside each of these runs.
agent-playbooks
Code