Automated email support pipeline with LLM-powered classification and human-in-the-loop approval.
Stop manually answering the same support emails over and over. This tool watches your inbox, classifies incoming emails against a knowledge base of reference responses, drafts replies using an LLM, and posts them to Discord for your team to review before sending. Fully config-driven β works for any SaaS product.
The workflow runs as a simple state machine, driven by a cron job:
IDLE β PENDING β DRAFTED β AWAITING β (approve) β IDLE
β β β β
β fetch β classify β notify β human review
β inbox β via LLM β Discord β approve/edit/reject
- IDLE β Polls your IMAP inbox for new emails
- PENDING β New emails found, sends them to the LLM for classification
- DRAFTED β LLM returns draft replies, posts previews to Discord
- AWAITING β Waits for a human to approve, edit, or reject each draft
- On approval, the reply is sent via SMTP and the cycle resets
Each cron run advances the state by one step. Spam and automated notifications are detected and ignored automatically.
- IMAP polling β Watches any IMAP mailbox for new support emails
- LLM classification β Matches emails against your knowledge base and drafts context-aware replies
- Multi-language β Detects the sender's language and replies in kind
- Human-in-the-loop β Nothing gets sent without explicit approval
- Discord notifications β Posts draft previews with original email + proposed reply for team review
- Knowledge base growth β Optionally adds approved replies back to the reference library (
--add-ref) - Config-driven β Works for any product; just update your
.envand reference responses - Multi-instance β Configurable
DATA_DIRlets you run separate instances per product/agent - JSON config alternative β Pass a
config.jsoninstead of (or alongside) environment variables - File-based state β No database required; state is persisted as simple JSON files
- Cron-friendly β Designed to run every few minutes via cron or systemd timer
- Node.js 18+ (uses native
fetch) - An IMAP/SMTP email account (e.g., Gmail, Fastmail, any provider)
- openclaw running locally (for LLM access), or adapt
src/llm.jsfor direct API calls - A Discord channel + bot (optional, for notifications)
# Clone the repository
git clone https://github.com/boris721/email-workflow-support.git
cd email-workflow-support
# Install dependencies
npm install
# Copy and configure environment
cp .env.example .env
# Edit .env with your credentials
# Create your knowledge base (or copy the example)
cp reference-responses.example.json data/reference-responses.json
# (Optional) Make the CLI globally available
npm link
# Run the workflow
./bin/email-workflow cronThe first run establishes a UID baseline for your inbox β it won't process old emails. Subsequent runs will pick up anything new.
Configuration is loaded from environment variables (.env) and/or a JSON config file. The JSON file takes precedence where both are set.
See .env.example for a fully commented template. Key variables:
| Variable | Description |
|---|---|
SERVICE_NAME |
Your product/service name (used in logs) |
IMAP_HOST / IMAP_PORT |
IMAP server connection |
SMTP_HOST / SMTP_PORT |
SMTP server connection |
EMAIL_USER / EMAIL_PASS |
Email account credentials (used for both IMAP & SMTP) |
SMTP_FROM_NAME |
Display name for outgoing emails (e.g., "Acme Support") |
OPENCLAW_GATEWAY_PORT |
Port for the openclaw gateway (default: 18789) |
OPENCLAW_GATEWAY_TOKEN |
Auth token for the gateway (or use CLAWD_TOKEN) |
REFERENCES_FILE |
Path to your reference responses JSON |
DISCORD_CHANNEL_ID |
Discord channel for draft notifications |
DATA_DIR |
Custom data directory path (default: ./data/). Allows multiple instances with separate state. |
Pass --config path/to/config.json to the CLI. See config.example.json for the full structure.
./bin/email-workflow cron --config ./my-config.json| Command | Description |
|---|---|
cron |
Run one cycle of the state machine (fetch β classify β notify) |
approve [--uid N] |
Approve and send draft(s). Omit --uid to approve all. |
approve --uid N --add-ref |
Approve, send, and add the response to the knowledge base |
edit --uid N --body "..." |
Edit a draft's reply text (triggers re-notification on next cron) |
Tip: Use
\\nliterals for newlines in the body, e.g.,edit --uid 42 --body "Hello,\n\nThank you for reaching out.\n\nBest regards,\nSupport Team"| |reject [--uid N]| Reject draft(s) without sending. Omit--uidto reject all. |
All commands accept --config <path> to load a JSON config file.
# Run the main loop
./bin/email-workflow cron
# Approve a specific draft
./bin/email-workflow approve --uid 42
# Approve and learn from it
./bin/email-workflow approve --uid 42 --add-ref
# Edit before approving
./bin/email-workflow edit --uid 42 --body "Thanks for reaching out! ..."
# Reject a draft
./bin/email-workflow reject --uid 42bin/
email-workflow # CLI entry point (Commander.js)
src/
config.js # Loads config from .env + optional JSON file
imap.js # IMAP service β connects, fetches new emails
send.js # SMTP service β sends approved replies
classify.js # LLM classifier β matches emails to references, drafts replies
llm.js # openclaw gateway HTTP client (llm-task tool)
reference.js # Reference library β load/save/add entries
state.js # File-based state machine (IDLE β PENDING β DRAFTED β AWAITING)
notify.js # Discord notification service
index.js # Barrel export
data/
reference-responses.json # Your knowledge base (gitignored)
pending-emails.json # Transient: emails awaiting classification
drafts.json # Transient: LLM-generated draft replies
imap-state.json # Tracks last seen IMAP UID
test/
classify.test.js # Classifier unit tests
imap.test.js # IMAP service tests
state.test.js # State machine tests
The workflow posts draft previews to a Discord channel so your team can review them. Each notification includes:
- The original email (sender, subject, body excerpt)
- The LLM's classification (category, confidence, language)
- The proposed reply
Your team can then use CLI commands (approve, edit, reject) to take action β or you can wire up a Discord bot to handle these commands directly.
Setup:
- Create a Discord bot and add it to your server
- Set
DISCORD_CHANNEL_IDin your.env - The notification service uses openclaw's
message sendCLI under the hood
This project uses openclaw's llm-task gateway to access LLMs. The gateway runs locally and provides a unified HTTP API for structured JSON responses with schema validation.
How it works:
src/llm.jssends a POST request tohttp://127.0.0.1:{port}/tools/invokewith the prompt, input data, and a JSON schema- The gateway routes the request to the configured LLM provider (OpenAI, Anthropic, etc.)
- The response is validated against the schema and returned as structured JSON
This is optional. If you don't want to use openclaw, you can replace src/llm.js with direct calls to the OpenAI or Anthropic API. The callLlmTask function has a simple interface β just swap the HTTP call for your preferred SDK.
The knowledge base is a JSON array of reference entries. Each entry describes a support topic with metadata and pre-written responses in one or more languages.
See reference-responses.example.json for the format.
{
"id": "password-reset",
"category": "account",
"keywords": ["password", "reset", "forgot", "login"],
"languages": ["en", "de"],
"question_summary": "User wants to reset their password",
"reference_response_en": "To reset your password, go to ...",
"reference_response_de": "Um Ihr Passwort zurΓΌckzusetzen, gehen Sie zu ..."
}Fields:
idβ Unique kebab-case identifiercategoryβ Topic category (e.g.,account,billing,technical,general)keywordsβ Search terms for matching (multi-language)languagesβ Which languages have reference responsesquestion_summaryβ One-line description of the support topicreference_response_{lang}β The template response in each language
The LLM uses these as templates β it adapts the tone and content to each specific email rather than copy-pasting them verbatim.
When you approve a draft with --add-ref, the system uses the LLM to generate metadata and adds the new entry automatically.
The workflow is designed to run on a schedule. Each invocation advances the state machine by one step.
# Edit your crontab
crontab -e
# Run every 5 minutes
*/5 * * * * cd /path/to/email-workflow-support && /usr/local/bin/node ./bin/email-workflow cron >> ./logs/cron.log 2>&1Tips:
- Use full paths in cron (Node.js binary, project directory)
- Redirect output to a log file for debugging
- The state machine is idempotent β running it more frequently than needed is harmless
- If you need the openclaw gateway, make sure it's running before the cron fires
# Run all tests
npm test
# Run with coverage
npx vitest run --coverageTests are in the test/ directory and use Vitest.
Contributions are welcome! This project is intentionally simple and modular β feel free to open issues or PRs.
Some ideas:
- Slack/Telegram notification adapters
- Web UI for draft review
- Direct OpenAI/Anthropic SDK integration (replacing the gateway dependency)
- Email threading and conversation history
- Attachment handling
MIT Β© 2026 Paul Panserrieu & Boris