Guide
Build a Rivet Email Agent
Wire Nylas CLI email into a Rivet graph through a host function, returning JSON so downstream nodes can classify, draft, audit, and pause for human review.
Written by Pouya Sanooei Software Architect
Command references used in this guide: nylas email list, nylas email search, and nylas email drafts create.
How do you give a Rivet graph email?
You give a Rivet graph email by making mailbox access a graph node, not hidden application code. The node runs one CLI command, returns JSON, and connects to later Rivet nodes that classify, summarize, or draft. That keeps a 6-provider mailbox action visible in the same visual graph as the agent's prompt steps.
Rivet is Ironclad's open-source visual IDE for building AI agent graphs. A project can contain multiple graphs, each graph is made of connected nodes, and @ironclad/rivet-node can run a graph from a TypeScript host app. Email fits that model well: authenticate once with nylas auth login, then let the graph call the tool for each read or draft action.
How do you model email as a Rivet node?
Model email in Rivet as a node that calls a narrow host function with 1 job: run a command and emit stdout. Rivet's in-graph Code node runs sandboxed JavaScript with no child_process access, so the subprocess lives in your TypeScript host, not the node itself. The graph should show a reader node, a decision node, and a draft node as separate boxes, which makes the email step easy to inspect in the editor before anyone runs the graph in production.
The reader node below runs nylas email list because inbox triage starts with a bounded batch, not a full mailbox crawl. Use --limit 10 for the first graph run so the LLM sees only 10 recent messages, then increase the number after the node output shape is stable. The --json flag gives the next node structured data.
# Host function behind the Rivet reader node: read a small inbox batch as JSON.
nylas email list --json --limit 10Name the node something literal, like read_recent_email, and label the outgoing edge messages_json. In a visual review, a teammate can see that this node only reads mail and that its output flows forward as data. The graph stays honest because the node label, command, and edge all describe the same operation.
How does a Rivet SDK runner call email commands?
A Rivet SDK runner can execute the graph while your TypeScript host owns the shell boundary. Put the subprocess wrapper in the host process, expose it to the graph as a named host function the node calls, and return plain JSON. That keeps credentials and 1 authenticated grant outside the LLM prompt.
This wrapper runs nylas email search when the graph needs a smaller slice than the latest inbox page. Search is useful after a classifier finds a sender or subject worth deeper review, and --limit 5 caps the follow-up to 5 messages. The function returns stdout, so Rivet can pass it to the next node.
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const run = promisify(execFile);
export async function searchEmail(query: string) {
const { stdout } = await run("nylas", [
"email",
"search",
query,
"--json",
"--limit",
"5",
]);
return stdout;
}When you run the graph with runGraphInFile, map the Rivet node to this host function. The visual node remains the agent's email action, while the TypeScript file handles quoting, timeouts, logging, and exit-code errors in one place. That split is easier to test than embedding provider SDK code in every graph.
How does graph output flow into the next Rivet node?
Graph output should move from command stdout to a parser node, then to a decision node with a typed instruction. Rivet's value is the visible edge: reviewers can trace a message from read_recent_email to classify_priority to draft_reply. For a daily triage graph, that's 3 auditable decisions instead of one hidden script.
Run nylas email read only after a prior node selects a message ID from the list or search result. The command below reads exactly 1 message by ID, so the prompt node receives the full body only for the chosen thread. Pipe to jq when you want to keep just fields the next node needs.
MESSAGE_ID="msg_abc123"
nylas email read "$MESSAGE_ID" --json | jq '{id, from, subject, body}'In the Rivet editor, connect that filtered JSON to a Chat node with a short schema-like instruction: classify as urgent, waiting, or archive, then return a reason under 20 words. The next node should consume the decision object, not re-read the inbox. Data flows forward, and each edge has a clear meaning.
Why does the Rivet graph make email decisions auditable?
Rivet makes email decisions auditable because the agent's path is a node graph you can inspect before and after a run. A reviewer sees the command node, the prompt node, the model output node, and the draft node as separate steps. That matters when one wrong email can cost hours of follow-up.
Add a small audit output node after every high-impact decision. Store the command name, message ID, classification, and draft ID in 1 JSON object, then log it from the host app. For example, a review record can say that nylas email search found 5 messages, the classifier picked 1, and the draft node created 1 draft for human review.
This is the main difference from a plain script. A script can log the same facts, but the graph shows why the agent reached the draft step. The edge from classifier to draft is visible, so a team can review prompt wording, node order, and data shape without reading the whole TypeScript runner.
What guardrails should the Rivet graph have?
A Rivet graph that reads email needs guardrails at the node boundary. The risky pattern is the lethal trifecta: private data + untrusted content + external communication. Keep autonomous runs on read and draft commands, and require a person before delivery. That turns a bad model decision into 1 reviewable draft instead of a sent message.
Use nylas email drafts create for any reply the graph prepares, because that command creates a draft instead of sending mail. Include --reply-to when the draft belongs to a selected message ID, and keep the body generated by a prior node under review. OWASP LLM01 (2025) names prompt injection as the top LLM risk, so never let email text authorize new capabilities.
nylas email drafts create \
--reply-to "$MESSAGE_ID" \
--to "ops@example.com" \
--subject "Re: Incident follow-up" \
--body "Drafted response for human review." \
--jsonAdd 2 more checks in the host runner: allow-list command names and cap result sizes. A graph node should be able to call email list, email search, email read, or drafts create, but not arbitrary shell text. For broader containment, see stop an AI agent going rogue and build a human-in-the-loop email agent.
Next steps
- Build a Vellum Email Agent — compare the same email node pattern in another visual workflow builder
- Build a ControlFlow Email Agent — move from visual graph nodes to Python task orchestration
- Build a human-in-the-loop email agent — design review queues before sending
- Stop an AI agent going rogue — contain tools, prompts, and external actions
- Full command reference — check every command and flag before wiring a graph node