If you speak at conferences, you know the Call for Papers (CFP) hunt is a grind. Deadlines are scattered across a dozen sites, half of them are already closed, and you can never remember which ones you've already seen or applied to. I was doing this by hand every week... and I was still missing good ones.
So I built an agent to do it for me. It runs entirely serverless on AWS, wakes up on a schedule, pulls CFPs from multiple sources, ranks them against my speaker profile with a model on Amazon Bedrock, and drops the good ones in Slack. This post walks through how it's put together and, more importantly, why I picked the AWS pieces I did.
WHAT I BUILT
The whole thing is a pipeline that runs on its own every morning:
- Fetch open CFPs from a few conference aggregators
- Normalize them into one consistent shape
- Filter them against rules I define (topics, countries, delivery type, deadlines)
- Rank the survivors against my speaker profile using a model on Bedrock
- Store the results as JSON and notify me in Slack
There's no server to babysit. When it's not running, it costs me basically nothing. That's the part I want to focus on... because the AWS choices are what make an "agent that runs every day" something you can build on a personal budget.
THE PROBLEM UP CLOSE
Here's what the manual version looked like. Open five or six tabs, scroll through conference listings, cross-reference against a note somewhere of what I'd already applied to, do the math on whether the deadline had even passed, and then repeat the whole thing next week because new ones show up constantly.
The worst part was memory. A CFP I looked at last Tuesday looked brand new again the following Monday. I had no durable sense of "this is new since I last checked" or "I already said no to this one." That's exactly the kind of stateful, repetitive, run-it-on-a-schedule chore that a little bit of AWS glue is perfect for.
THE AWS ARCHITECTURE
Here's the shape of it. Every box is serverless or managed, so there's nothing running when the job isn't.
EventBridge Scheduler (cron)
│
▼
Lambda "cfp-agent"
│
┌─────┼───────────────┐
▼ ▼ ▼
feeds DynamoDB Bedrock
(fetch) (rules, (rank +
profile, recommend)
seen log)
│
┌────┴────┐
▼ ▼
S3 json Slack
(dashboard) (digest)
Let me walk through the important pieces.
Lambda is the whole engine
The entire pipeline is one Lambda function. Fetch, normalize, filter, rank, write... it all happens inside a single handler. There's no always-on box, no container sitting idle overnight. It spins up, does the work in a minute or two, and goes back to sleep.
The handler is really just an orchestration of the steps:
def lambda_handler(event, context):
cfps = fetch_all_sources() # pull the feeds
cfps = normalize(cfps) # one consistent schema
cfps = tag_against_rules(cfps) # topics, countries, deadlines
ranked = rank_with_bedrock(cfps) # score vs my profile
write_to_s3(ranked) # dashboard reads this
notify_slack(ranked) # ping me on the good ones
return {"count": len(ranked)}
This is the thing I love about Lambda for a personal agent... the unit of deployment is just the function. I'm not paying for uptime, I'm paying for the ninety seconds a day it actually runs.
DynamoDB is the agent's memory
This was the fix for my biggest pain point. The model doesn't remember anything between runs, so I give it memory in DynamoDB. One small table holds three things: my filter rules, my speaker profile, and a "seen" log of every CFP id I've already surfaced.
import boto3
table = boto3.resource("dynamodb").Table("cfp-agent")
# the agent's "memory": rules, profile, and what it's already seen
rules = table.get_item(Key={"id": "rules"})["Item"]
profile = table.get_item(Key={"id": "profile"})["Item"]
seen = table.get_item(Key={"id": "seen"})["Item"]
# after the run, remember what we surfaced so tomorrow knows what's new
table.put_item(Item={"id": "seen", "discovered": updated_seen})
The seen log is what lets the agent tell me "here's what's new since yesterday" instead of showing me the same list forever. DynamoDB on pay-per-request pricing is a great fit here... a handful of tiny reads and writes a day rounds down to nothing, and I never think about capacity.
Bedrock does the ranking
Deterministic rules do the cheap first cut... drop anything closed, wrong country, wrong topic. But "is this conference actually a good fit for me?" is a judgment call, not a keyword match. That's where a model on Amazon Bedrock comes in.
I feed it my profile (bio, expertise, goals, past talks) and each surviving CFP, and it hands back a relevance score, a recommendation of apply / consider / skip, and a short reason. Because the model is stateless, that profile from DynamoDB is what makes the recommendations actually about me.
prompt = f"""You match conference CFPs to a speaker.
SPEAKER PROFILE:
{profile}
Score each CFP 0-100 for fit. Return apply / consider / skip
plus a one-line reason.
CFPS:
{cfp_batch}
"""
resp = bedrock.invoke_model(modelId=MODEL_ID, body=json.dumps(payload))
One thing I decided early: no silent fallback. If Bedrock can't be reached, the run fails loudly and writes an error the dashboard shows as a banner. I'd rather know the ranking is broken than get a quietly degraded list of keyword-matched guesses that looks fine but isn't.
EventBridge Scheduler makes it an agent, not a script
The thing that turns this from "a script I run when I remember" into "an agent that just handles it" is EventBridge Scheduler. A cron expression kicks the Lambda off every morning. It's timezone-aware, so it fires at 6am my time without me doing daylight-saving math.
cron(0 6 * * ? *) # 6:00am, every day
That's it. That single line is the difference between a tool I have to remember to use and one that quietly does its job before I wake up.
S3 + CloudFront for the dashboard
The Lambda writes the results as a JSON file to S3, and a static HTML dashboard reads it through CloudFront. No API, no database query from the browser... just a JSON file on a CDN. The dashboard is where I actually browse, search, hide ones I'm not into, and star the ones I want to apply to. Cheap, fast, and nothing to run.
DOES IT WORK?
Yeah. It went from me manually combing sites every week to opening one dashboard where the good ones are already ranked and the new ones are flagged. On a recent run it pulled well over a thousand CFPs across sources, cut them down to a few hundred that passed my rules, and Bedrock flagged a few dozen worth actually applying to. That's the whole point... I look at the dozen, not the thousand.
And because it remembers what it's shown me, I finally have that "new since last time" signal I was missing. No more re-reading the same listings and wondering if I'd already seen them.
THINGS TO CONSIDER
Feed data can be wrong. Community-maintained feeds sometimes have stale or flat-out incorrect deadlines. I added a separate verify step that actually renders the CFP page to double-check the date, but it's a reminder that your agent is only as good as its inputs... build in a way to correct them.
Bedrock costs scale with volume. Ranking a few hundred CFPs a day is cheap, but it's not free. Batching the calls and doing the cheap rule-based filtering first keeps the model spend down. Don't send the model everything... send it what already survived the free checks.
Serverless state needs a home. The moment your agent needs to remember anything between runs, you need somewhere durable to put it. DynamoDB was the low-friction answer for me, but the lesson is to plan for state early rather than bolting it on later.
GET STARTED
If you've got a repetitive, run-it-on-a-schedule chore that also needs a little judgment, this shape is worth stealing. Lambda for the work, DynamoDB for the memory, Bedrock for the judgment calls, EventBridge to run it on its own, and S3 for the output. Every piece scales to zero when it's idle, so an agent that runs every single day can cost next to nothing.
I built this because I was tired of missing CFPs... now I just check a dashboard. If you speak at conferences, or want to, that alone is worth the few hours it takes to set this up.
Top comments (0)