Guide
OpenAI Assistants Email Tools (CLI)
OpenAI's function calling lets a model ask your code to run a named function with structured arguments. That's exactly the seam you need to give an assistant email: define a few tool schemas, and dispatch each call to the Nylas CLI instead of writing a provider SDK. The model decides what to do; the CLI does it across six providers and returns JSON. This guide wires function calling to the CLI and keeps sends behind a human.
Written by Pouya Sanooei Software Engineer
Command references used in this guide: nylas email list, nylas email search, and nylas email drafts create.
How do you give an OpenAI assistant email?
You give an OpenAI assistant email by declaring function-calling tools and routing each call to the Nylas CLI. A tool is a JSON schema describing a name, a description, and parameters; when the model returns a tool_calls entry, your code runs the matching command and returns the output as a tool message. Because nylas email list --json emits structured data, the model gets clean JSON to reason over.
Authenticate the CLI once with nylas auth login; the stored grant is reused on every subprocess call, so your dispatcher never handles credentials. OpenAI's function-calling contract — tool schemas in, tool_calls out, tool results back — is documented in the OpenAI function calling guide.
How do you define the tool schemas?
Define one schema per action so the model has clear, narrow capabilities. A list_unread tool takes an integer limit; a search_email tool takes a query string. Keep descriptions specific — “list unread emails as JSON” — because the model selects tools from those descriptions. Two read tools plus a draft tool cover most inbox assistants.
The schemas are pure data; they don't run anything until your dispatcher maps a tool name to a CLI command. That separation keeps the model's view simple and the execution auditable: every action is one command with explicit arguments you can log. Define no more than a handful of tools — a smaller surface is easier for the model to use correctly.
tools = [
{
"type": "function",
"function": {
"name": "list_unread",
"description": "List unread emails as JSON across the connected mailbox.",
"parameters": {
"type": "object",
"properties": {"limit": {"type": "integer", "default": 10}},
},
},
},
{
"type": "function",
"function": {
"name": "search_email",
"description": "Search the mailbox with a provider-agnostic query, return JSON.",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
},
},
]How do you dispatch tool calls to the CLI?
Dispatch by mapping each tool name to a CLI command, running it as a subprocess, and returning stdout as the tool result. The loop is: send the message, read any tool_calls, run them, append the results, and call the model again until it answers. A triage request resolves in two or three round trips, with the model reading JSON between each.
Keep the dispatcher a thin switch — name to command — so it stays easy to audit. The model never sees a provider name; it calls search_email, and your code runs nylas email search. That indirection is what lets one assistant work against Gmail, Outlook, and four more backends without changing a line of agent code.
import subprocess, json
from openai import OpenAI
client = OpenAI()
def run_tool(name, args):
if name == "list_unread":
cmd = ["nylas", "email", "list", "--unread", "--json",
"--limit", str(args.get("limit", 10))]
elif name == "search_email":
cmd = ["nylas", "email", "search", args["query"], "--json", "--limit", "20"]
else:
return "unknown tool"
return subprocess.run(cmd, capture_output=True, text=True, check=True).stdout
msgs = [{"role": "user", "content": "Summarize my unread mail."}]
resp = client.chat.completions.create(model="gpt-4o", messages=msgs, tools=tools)
# Read resp tool_calls, run_tool() each, append results, call again until done.How do you keep sends safe?
Keep outbound actions behind a human. Expose a draft tool that runs nylas email drafts create — it composes a message without sending and returns a draft ID — instead of a send tool. A person reviews the draft and sends it, so a misclassification can't put mail in a customer's inbox. This is the single most important guardrail for an email assistant.
The model reads untrusted content, and a prompt injection in a message body can try to add an unwanted tool_call. Containment outside the model's reasoning — a review step, or connector-level rules — holds even when injected text tries to talk past it. For deterministic enforcement at the connector, see stopping a rogue agent at the connector layer.
Next steps
- Anthropic tool use email — the same pattern with Claude's Messages API
- Connect Claude to email — MCP-based wiring instead of function calling
- Human-in-the-loop email agent — draft-and-approve guardrails
- Build an AI email triage agent — classification and routing end to end
- Semantic Kernel email agent — the CLI as a native plugin
- Full command reference — every flag and subcommand documented