
hermes-marketing-dashboard
Open-source marketing operations control center for AI agent teams with CRM, outreach, content ops, and analytics powered by OpenClaw + SQLite
- Installs
- 1K
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/reason-machines/marketing-skills --skill hermes-marketing-dashboard- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads hermes-marketing-dashboard automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Hermes Marketing Dashboard
Skill by ara.so — Marketing Skills collection.
Hermes Dashboard is an open-source marketing operations control center designed for AI agent teams. It provides CRM, outreach sequencing, content operations, analytics, and automation workflows in a single Next.js application powered by OpenClaw integration and local SQLite storage.
Installation
Prerequisites
- Node.js 18+
- pnpm (required) - install with
npm install -g pnpmorcorepack enable - OpenClaw CLI (optional but recommended for agent integration)
Quick Start
git clone https://github.com/builderz-labs/marketing-dashboard.git
cd marketing-dashboard
pnpm install
pnpm env:bootstrap
pnpm dev
The application will start at http://localhost:3000.
Configuration
Required Environment Variables
Create a .env.local file:
# Authentication (required)
AUTH_USER=admin
AUTH_PASS=your-secure-password-min-10-chars
API_KEY=your-api-key-for-programmatic-access
# Cookie security (false for HTTP local, true for HTTPS production)
AUTH_COOKIE_SECURE=false
# Database (auto-created in ./state)
DATABASE_URL=./state/hermes.db
OpenClaw Integration
For AI agent integration with OpenClaw:
# OpenClaw home directory
HERMES_OPENCLAW_HOME=/path/to/openclaw
# Default instance name
HERMES_DEFAULT_INSTANCE=main
# Multi-instance support (optional JSON array)
HERMES_OPENCLAW_INSTANCES='[{"name":"prod","path":"/openclaw/prod"},{"name":"dev","path":"/openclaw/dev"}]'
Security Configuration
# Host access lock (default: local-only)
HERMES_HOST_LOCK=local # or 'off' or 'host1,host2'
# Writeback protection (keep false unless explicitly needed)
HERMES_ALLOW_POLICY_WRITE=false
HERMES_ALLOW_CRON_WRITE=false
HERMES_ALLOW_WORKSPACE_WRITE=false
Optional Analytics Integration
# Plausible Analytics
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=yourdomain.com
PLAUSIBLE_API_KEY=your-plausible-api-key
# Google Analytics 4
NEXT_PUBLIC_GA4_ID=G-XXXXXXXXXX
1Password Runtime Overlay (Optional)
HERMES_1PASSWORD_MODE=auto # off|auto|required
HERMES_OP_ENV_FILE=/etc/hermes-dashboard/hermes-dashboard.op.env
Key Commands
Development
# Start development server
pnpm dev
# Build for production
pnpm build
# Start production server
pnpm start
# Type checking
pnpm typecheck
# Linting
pnpm lint
# Run tests
pnpm test
# Run end-to-end tests
pnpm test:e2e
Database Management
# Bootstrap environment and database
pnpm env:bootstrap
# Reset database (warning: destructive)
pnpm db:reset
Template Export
# Audit template for sensitive data
./scripts/template-audit.sh
# Export clean template
./scripts/template-export.sh /path/to/output
Core API Patterns
CRM Lead Management
// app/api/crm/leads/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getDatabase } from '@/lib/db';
export async function GET(request: NextRequest) {
const db = getDatabase();
const leads = db.prepare(`
SELECT id, email, name, source, status, created_at
FROM crm_leads
WHERE status = ?
ORDER BY created_at DESC
`).all('active');
return NextResponse.json({ leads });
}
export async function POST(request: NextRequest) {
const db = getDatabase();
const { email, name, source, metadata } = await request.json();
const result = db.prepare(`
INSERT INTO crm_leads (email, name, source, metadata, status)
VALUES (?, ?, ?, ?, 'new')
`).run(email, name, source, JSON.stringify(metadata));
return NextResponse.json({ id: result.lastInsertRowid }, { status: 201 });
}
Outreach Sequencing
// lib/outreach/sequence.ts
import { getDatabase } from '@/lib/db';
export interface OutreachSequence {
id: number;
name: string;
steps: OutreachStep[];
status: 'active' | 'paused' | 'archived';
}
export interface OutreachStep {
order: number;
delay_hours: number;
template_id: string;
channel: 'email' | 'linkedin' | 'twitter';
}
export function createSequence(
name: string,
steps: OutreachStep[]
): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO outreach_sequences (name, steps, status)
VALUES (?, ?, 'active')
`).run(name, JSON.stringify(steps));
return result.lastInsertRowid as number;
}
export function enrollInSequence(
leadId: number,
sequenceId: number
): void {
const db = getDatabase();
db.prepare(`
INSERT INTO outreach_enrollments (lead_id, sequence_id, current_step, status)
VALUES (?, ?, 0, 'active')
`).run(leadId, sequenceId);
}
export function pauseSequence(sequenceId: number): void {
const db = getDatabase();
db.prepare(`
UPDATE outreach_sequences
SET status = 'paused'
WHERE id = ?
`).run(sequenceId);
}
Content Operations
// lib/content/calendar.ts
import { getDatabase } from '@/lib/db';
export interface ContentItem {
id: number;
title: string;
type: 'blog' | 'social' | 'email' | 'video';
status: 'draft' | 'scheduled' | 'published';
scheduled_at?: Date;
published_at?: Date;
metadata: Record<string, any>;
}
export function getContentCalendar(
startDate: Date,
endDate: Date
): ContentItem[] {
const db = getDatabase();
const items = db.prepare(`
SELECT id, title, type, status, scheduled_at, published_at, metadata
FROM content_items
WHERE scheduled_at BETWEEN ? AND ?
ORDER BY scheduled_at ASC
`).all(startDate.toISOString(), endDate.toISOString());
return items.map(item => ({
...item,
metadata: JSON.parse(item.metadata as string),
scheduled_at: item.scheduled_at ? new Date(item.scheduled_at) : undefined,
published_at: item.published_at ? new Date(item.published_at) : undefined,
}));
}
export function createContentItem(item: Omit<ContentItem, 'id'>): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO content_items (title, type, status, scheduled_at, metadata)
VALUES (?, ?, ?, ?, ?)
`).run(
item.title,
item.type,
item.status,
item.scheduled_at?.toISOString(),
JSON.stringify(item.metadata)
);
return result.lastInsertRowid as number;
}
OpenClaw Agent Integration
// lib/openclaw/agents.ts
import { readdir, readFile } from 'fs/promises';
import { join } from 'path';
export interface OpenClawAgent {
name: string;
description: string;
type: 'agent' | 'squad';
capabilities: string[];
config: Record<string, any>;
}
export async function discoverAgents(
openclawHome: string
): Promise<OpenClawAgent[]> {
const agentsDir = join(openclawHome, 'agents');
const agents: OpenClawAgent[] = [];
try {
const entries = await readdir(agentsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const configPath = join(agentsDir, entry.name, 'agent.json');
try {
const configData = await readFile(configPath, 'utf-8');
const config = JSON.parse(configData);
agents.push({
name: entry.name,
description: config.description || '',
type: config.type || 'agent',
capabilities: config.capabilities || [],
config,
});
} catch (err) {
console.warn(`Could not load agent config for ${entry.name}`);
}
}
}
} catch (err) {
console.error('Failed to discover agents:', err);
}
return agents;
}
Cron Job Management
// lib/cron/scheduler.ts
import { getDatabase } from '@/lib/db';
export interface CronJob {
id: number;
name: string;
schedule: string; // cron expression, 'every 1h', or 'at 09:00'
agent: string;
task: string;
enabled: boolean;
last_run?: Date;
}
export function createCronJob(job: Omit<CronJob, 'id'>): number {
const db = getDatabase();
const result = db.prepare(`
INSERT INTO cron_jobs (name, schedule, agent, task, enabled)
VALUES (?, ?, ?, ?, ?)
`).run(job.name, job.schedule, job.agent, job.task, job.enabled ? 1 : 0);
return result.lastInsertRowid as number;
}
export function getCronJobs(): CronJob[] {
const db = getDatabase();
const jobs = db.prepare(`
SELECT id, name, schedule, agent, task, enabled, last_run
FROM cron_jobs
ORDER BY name ASC
`).all();
return jobs.map(job => ({
...job,
enabled: Boolean(job.enabled),
last_run: job.last_run ? new Date(job.last_run) : undefined,
}));
}
export function updateLastRun(jobId: number): void {
const db = getDatabase();
db.prepare(`
UPDATE cron_jobs
SET last_run = ?
WHERE id = ?
`).run(new Date().toISOString(), jobId);
}
Authentication Patterns
Session-Based Auth
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('hermes-session');
if (!session && request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.next();
}
export const config = {
matcher: '/api/:path*',
};
API Key Auth
// lib/auth/api-key.ts
import { NextRequest } from 'next/server';
export function validateApiKey(request: NextRequest): boolean {
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
}
const token = authHeader.substring(7);
return token === process.env.API_KEY;
}
// Usage in API route
export async function GET(request: NextRequest) {
if (!validateApiKey(request)) {
return NextResponse.json({ error: 'Invalid API key' }, { status: 401 });
}
// Continue with request...
}
Analytics Integration
Track Events
// lib/analytics/events.ts
import { getDatabase } from '@/lib/db';
export interface AnalyticsEvent {
event_type: string;
properties: Record<string, any>;
user_id?: string;
session_id?: string;
}
export function trackEvent(event: AnalyticsEvent): void {
const db = getDatabase();
db.prepare(`
INSERT INTO analytics_events (event_type, properties, user_id, session_id, timestamp)
VALUES (?, ?, ?, ?, ?)
`).run(
event.event_type,
JSON.stringify(event.properties),
event.user_id,
event.session_id,
new Date().toISOString()
);
}
export function getKPIs(startDate: Date, endDate: Date) {
const db = getDatabase();
return {
leads: db.prepare(`
SELECT COUNT(*) as count FROM crm_leads
WHERE created_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
outreach_sent: db.prepare(`
SELECT COUNT(*) as count FROM outreach_messages
WHERE sent_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
content_published: db.prepare(`
SELECT COUNT(*) as count FROM content_items
WHERE published_at BETWEEN ? AND ?
`).get(startDate.toISOString(), endDate.toISOString()),
};
}
Component Patterns
Dashboard Widget
// components/dashboard/LeadsFunnel.tsx
'use client';
import { useEffect, useState } from 'react';
interface FunnelData {
stage: string;
count: number;
}
export function LeadsFunnel() {
const [data, setData] = useState<FunnelData[]>([]);
useEffect(() => {
fetch('/api/crm/funnel')
.then(res => res.json())
.then(setData);
}, []);
return (
<div className="funnel-widget">
<h3>Pipeline Funnel</h3>
{data.map(stage => (
<div key={stage.stage} className="funnel-stage">
<span>{stage.stage}</span>
<span>{stage.count}</span>
</div>
))}
</div>
);
}
Troubleshooting
Database Lock Errors
SQLite database locks can occur with concurrent writes:
// lib/db.ts
import Database from 'better-sqlite3';
let db: Database.Database | null = null;
export function getDatabase(): Database.Database {
if (!db) {
db = new Database(process.env.DATABASE_URL || './state/hermes.db');
db.pragma('journal_mode = WAL'); // Write-Ahead Logging for better concurrency
db.pragma('busy_timeout = 5000'); // 5 second timeout
}
return db;
}
OpenClaw Agent Discovery Fails
Ensure HERMES_OPENCLAW_HOME points to valid directory:
# Verify path
ls $HERMES_OPENCLAW_HOME/agents
# Check permissions
chmod -R u+r $HERMES_OPENCLAW_HOME/agents
Session Cookie Not Persisting
For HTTPS deployments, ensure:
AUTH_COOKIE_SECURE=true
For local HTTP development:
AUTH_COOKIE_SECURE=false
Host Lock Blocking Access
If you need to access from network:
# Disable (not recommended for production)
HERMES_HOST_LOCK=off
# Allow specific hosts
HERMES_HOST_LOCK=localhost,192.168.1.100,mydomain.com
API Authentication Failures
Verify API key in request headers:
curl -H "Authorization: Bearer $API_KEY" http://localhost:3000/api/crm/leads
Check environment variable is loaded:
// In API route
if (!process.env.API_KEY) {
console.error('API_KEY not configured');
}
Production Deployment
Security Checklist
-
Change default credentials:
AUTH_USER=your-admin-username AUTH_PASS=strong-password-min-10-chars API_KEY=cryptographically-secure-key -
Enable HTTPS cookie security:
AUTH_COOKIE_SECURE=true -
Keep host lock enabled:
HERMES_HOST_LOCK=yourdomain.com -
Keep writeback disabled unless required:
HERMES_ALLOW_POLICY_WRITE=false HERMES_ALLOW_CRON_WRITE=false HERMES_ALLOW_WORKSPACE_WRITE=false
Build and Deploy
pnpm build
pnpm start
For Docker deployment:
FROM node:18-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
EXPOSE 3000
CMD ["pnpm", "start"]
Common Patterns
Multi-Instance OpenClaw Setup
// lib/openclaw/instances.ts
export function getInstances(): Array<{name: string, path: string}> {
const instancesEnv = process.env.HERMES_OPENCLAW_INSTANCES;
if (instancesEnv) {
return JSON.parse(instancesEnv);
}
return [{
name: process.env.HERMES_DEFAULT_INSTANCE || 'main',
path: process.env.HERMES_OPENCLAW_HOME || './openclaw'
}];
}
Audit Logging
// lib/audit/logger.ts
import { getDatabase } from '@/lib/db';
export function logAuditEvent(
action: string,
userId: string,
metadata: Record<string, any>
): void {
const db = getDatabase();
db.prepare(`
INSERT INTO audit_log (action, user_id, metadata, timestamp)
VALUES (?, ?, ?, ?)
`).run(action, userId, JSON.stringify(metadata), new Date().toISOString());
}
FAQ
What does the hermes-marketing-dashboard skill do?
Open-source marketing operations control center for AI agent teams with CRM, outreach, content ops, and analytics powered by OpenClaw + SQLite
How do I install hermes-marketing-dashboard?
Run npx skills add https://github.com/reason-machines/marketing-skills --skill hermes-marketing-dashboard in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/hermes-marketing-dashboard/ (personal) or .claude/skills/hermes-marketing-dashboard/ (project).
Which AI agents can use hermes-marketing-dashboard?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is hermes-marketing-dashboard safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains hermes-marketing-dashboard?
It is published by reason-machines in reason-machines/marketing-skills. It has 1,030 recorded installs.
More from reason-machines/marketing-skills
marketing-for-founders-saas-growth
reason-machines/marketing-skills
Access curated marketing strategies and channels to acquire the first 10/100/1000 users for SaaS products and startups
agentkits-marketing-automation
reason-machines/marketing-skills
Enterprise AI marketing automation toolkit with 18 agents, 93 commands, and 28 skills for campaign planning, content creation, SEO, CRO, and growth workflows
composiohq-awesome-claude-skills-seo-content-marketing
reason-machines/marketing-skills
SEO & content marketing skill suite with keyword research, content audits, technical SEO, competitor analysis, and AI-powered content workflows
ai-marketing-skills-automation
reason-machines/marketing-skills
Open-source AI marketing automation skills for growth experiments, sales pipeline, content ops, outbound, SEO, and finance automation
marketingskills-ai-agent-collection
reason-machines/marketing-skills
Marketing skills collection for AI agents - CRO, copywriting, SEO, analytics, and growth engineering
ai-marketing-claude-suite
reason-machines/marketing-skills
Comprehensive AI marketing automation suite for Claude Code with 15 skills including website audits, copywriting, email sequences, ad campaigns, competitive analysis, and PDF reporting
Popular data & databases skills
supabase-postgres-best-practices
supabase/agent-skills
Postgres best practices maintained by Supabase, for Postgres running anywhere. Load this skill BEFORE writing or changing anything that lives in a Postgres database: creating or altering tables and columns (including choosing column types), schema design, migrations and declarative schema files, RLS policies and the tests that verify them, indexes, triggers, database functions, queues and scheduled jobs (pgcron, pgmq), vector/semantic search (pgvector), and restoring dumps (pgrestore) or importing data. Also load it when diagnosing slow queries, high CPU, timeouts, EXPLAIN plans, connection exhaustion, locking, bloat, or rows visible to the wrong user or tenant. This is not just a performance guide — schema, migration, security, and SQL authoring tasks need these rules too, even for a one-column change or a single query.
prisma-client-api
prisma/skills
Prisma Client API reference covering model queries, filters, operators, and client methods. Use when writing database queries, using CRUD operations, filtering data, or configuring Prisma Client. Triggers on "prisma query", "findMany", "create", "update", "delete", "$transaction".
prisma-database-setup
prisma/skills
Deprecated compatibility name for prisma-orm-setup. Use only when a user or existing integration explicitly invokes prisma-database-setup; otherwise select prisma-orm-setup.
prisma-cli
prisma/skills
Prisma ORM 7 CLI reference for existing Prisma 7 applications or explicitly selected Prisma 7 workflows. Covers prisma init, generate, migrate, db, dev, studio, and mcp. For setup, connection repair, or Prisma 8, use prisma-orm-setup; not the Prisma Platform CLI.
prisma-postgres
prisma/skills
Deprecated compatibility name for prisma-postgres-setup. Use only when a user or existing integration explicitly invokes prisma-postgres; otherwise select prisma-postgres-setup.
prisma-driver-adapter-implementation
prisma/skills
Required reference for Prisma ORM 7 SQL driver adapter work. Use when implementing or modifying adapters, adding database drivers, or touching SqlDriverAdapter, Transaction, savepoint, result mapping, or DriverAdapterError behavior. Covers current transaction lifecycle, optional savepoint hooks, original database-error preservation, and verification.