How I Used AI Agents to Automate My Marketing (With Code)
Running a solo app means wearing every hat: product, engineering, support, and marketing. Marketing is the one that most developers neglect, because it doesn't feel like "real work" and the results are invisible until they aren't.
I spent about three weeks building a multi-platform marketing automation system using AI agents, Python daemons, and the APIs for every major social platform. Here's the technical architecture and what actually worked.
The Problem: Marketing Is a Repeating Task Queue
Social media marketing for an indie app is roughly this loop:
- Produce content (posts, threads, articles)
- Post content on a schedule
- Engage with responses (replies, DMs, comments)
- Analyse what worked
- Repeat
Every part of this is automatable at some level. The creative/strategy layer still needs a human, but execution is a perfect target for automation.
Architecture: Daemons + Cron + AI Generation
The system I built has four layers:
┌────────────────────────────────────────────┐
│ Content Generation (AI, CrewAI pipeline) │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ Content Queue (JSON state files + lock) │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ Platform Posters (Python daemons, one per │
│ platform: Twitter, Threads, Facebook, │
│ Dev.to) │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ Scheduler (macOS LaunchAgents / cron) │
└────────────────────────────────────────────┘
Each platform poster is an independent Python script with its own state file. They share no runtime state — if one fails, the others continue. The state files are JSON, stored in ~/.susan-*.state.json per platform.
The State File Pattern
Every poster follows the same state file schema:
# Default state structure
DEFAULT_STATE = {
"posted_indices": [], # indices of already-posted items
"last_post_ts": 0, # Unix timestamp of last post
"post_count": 0, # total posts made
}
def load_state(state_file: str) -> dict:
if not os.path.exists(state_file):
return dict(DEFAULT_STATE)
try:
with open(state_file) as f:
data = json.load(f)
for key, val in DEFAULT_STATE.items():
data.setdefault(key, val)
return data
except (json.JSONDecodeError, OSError):
return dict(DEFAULT_STATE)
def save_state(state: dict, state_file: str) -> None:
"""Atomic write via temp file + rename."""
tmp = state_file + ".tmp"
with open(tmp, "w") as f:
json.dump(state, f, indent=2)
os.replace(tmp, state_file) # atomic on POSIX
The atomic write pattern (write tmp → rename) prevents state corruption if the process is interrupted mid-write.
Twitter Thread Poster
Twitter threads (8–12 tweets chained together) consistently outperform single tweets for educational content. I built a rotation system for 8 pre-written threads, each covering a different topic. A cooldown of 30 days prevents repeating a thread too soon.
THREADS = [
{
"id": "spaced_repetition",
"title": "How spaced repetition works",
"cooldown_days": 30,
"tweets": [
"A thread on why spaced repetition is the most evidence-backed study technique...",
"The core idea: your brain forgets in a predictable curve (Ebbinghaus, 1885)...",
# ...
]
},
# ...
]
def cmd_next():
state = load_state(STATE_FILE)
now = time.time()
available = [
t for t in THREADS
if now - state.get("last_posted", {}).get(t["id"], 0)
> t["cooldown_days"] * 86400
]
if not available:
return {"success": False, "error": "No threads available (all in cooldown)"}
thread = available[0]
result = post_thread(thread["tweets"])
if result["success"]:
state.setdefault("last_posted", {})[thread["id"]] = now
save_state(state, STATE_FILE)
return result
CrewAI for Weekly Content Generation
The manual content is a fixed rotation (fine for Twitter threads and tips, not ideal for generating fresh ideas). For weekly content generation, I built a CrewAI pipeline with four agents:
from crewai import Agent, Task, Crew
strategist = Agent(
role="Content Strategist",
goal="Identify the highest-value content topics for this week",
backstory="Expert in language learning content marketing...",
llm="anthropic/claude-sonnet-4-5-20250929",
)
writer = Agent(
role="Technical Writer",
goal="Write genuinely valuable technical content",
backstory="Developer and language learning enthusiast...",
llm="anthropic/claude-sonnet-4-5-20250929",
)
editor = Agent(
role="Editor",
goal="Polish content for platform-specific best practices",
backstory="Senior editor with deep knowledge of dev community...",
llm="anthropic/claude-sonnet-4-5-20250929",
)
distribution = Agent(
role="Distribution Manager",
goal="Format content for each target platform",
backstory="Social media specialist who knows platform nuances...",
llm="anthropic/claude-sonnet-4-5-20250929",
)
crew = Crew(
agents=[strategist, writer, editor, distribution],
tasks=[strategy_task, writing_task, editing_task, distribution_task],
verbose=True,
)
result = crew.kickoff(inputs={
"week": datetime.now().strftime("%Y-W%W"),
"app_focus": "Pocket Linguist language learning app",
"platforms": ["twitter", "devto", "linkedin"],
})
The pipeline runs weekly (Monday 8 AM via LaunchAgent) and produces a content bundle that the platform-specific posters can pull from.
LaunchAgent Scheduling
On macOS, LaunchAgents are the cron replacement for user-level scheduled tasks:
<!-- ~/Library/LaunchAgents/com.pocketlinguist.twitter.threads.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.pocketlinguist.twitter.threads</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/path/to/twitter_thread_poster.py</string>
<string>next</string>
</array>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Weekday</key><integer>2</integer> <!-- Tuesday -->
<key>Hour</key><integer>9</integer>
<key>Minute</key><integer>0</integer>
</dict>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>TWITTER_ACCESS_TOKEN</key>
<string>YOUR_TOKEN</string>
</dict>
<key>StandardOutPath</key>
<string>/Users/you/logs/twitter-threads.log</string>
<key>StandardErrorPath</key>
<string>/Users/you/logs/twitter-threads-error.log</string>
</dict>
</plist>
Load it with launchctl load ~/Library/LaunchAgents/com.pocketlinguist.twitter.threads.plist. Unload to pause.
Engagement Bot: Automated Replies
The engagement bot is the part that requires the most care. Automated replies that look spammy get accounts flagged. The rules I follow:
- Only reply to posts that match a keyword from a curated list (language learning topics)
- Use 36 different reply templates, selected semi-randomly to avoid pattern detection
- Rate limit to 20 replies per hour, with random delays between actions (jitter)
- Never DM unsolicited
- Log every action for review
REPLY_TEMPLATES = [
"That's a great point about {keyword}. In my experience...",
"This is exactly what motivated me to build {app}...",
# 34 more variants
]
def engage(tweet_id: str, keyword: str) -> dict:
template = random.choice(REPLY_TEMPLATES)
text = template.format(keyword=keyword, app="Pocket Linguist")
# Jitter: random delay 30–120 seconds
delay = random.uniform(30, 120)
time.sleep(delay)
return post_reply(tweet_id, text)
What Actually Moved the Needle
Honest assessment after three months:
- Twitter threads: Measurable engagement increase. Educational threads on language learning consistently reached 2–5x the impressions of single posts.
- Dev.to articles: Slow build but compounding. Articles rank in search and bring organic traffic weeks after publication.
- Threads (Meta): Highest organic reach of any platform, but the Threads API has reliability issues.
- Engagement bot: Modest follower growth. The quality of followers from bot engagement is lower than organic.
- CrewAI content generation: Saves 2–3 hours per week. Quality requires human review but the first draft is usually solid.
The biggest lesson: automation is best at distribution (consistent posting, scheduling), not at community building. The posts that drove real downloads were ones where I personally engaged with a large account's audience. Automation can prepare the content; the human moments convert.
I'm building Pocket Linguist, an AI-powered language tutor for iOS. It uses spaced repetition, camera translation, and conversational AI to help you reach conversational fluency faster. Try it free.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.