There’s a moment every developer knows. It’s 9:55am. Standup is in five minutes. You open a blank Slack message and try to remember what you actually did yesterday.
Was it the auth endpoint? The payment bug? That PR you reviewed? It all blurs together. You end up with something vague like “worked on backend stuff, no blockers” — which helps nobody, including you.
I got tired of that moment. So I built a 150-line Python agent to own it.
This is Part 4 of my Beginners Guide to Building AI Agents series. It’s the first part with a genuinely useful real-world application. No new concepts — just the same agent loop you already know, pointed at a problem you have every single day.
Why Standup Generation Is a Perfect First Real-World Agent
Before writing a line of code, I thought about what makes a good AI agent use case. Three criteria:
Repetitive and structured. The same task, same format, every day. This is where agents beat humans — consistency without fatigue.
Input is already captured somewhere. Your work notes exist. Your Jira tickets exist. The agent just needs to read them — it doesn’t need to invent anything.
Output goes somewhere useful. Not just printed to a terminal. Posted to your team channel, saved to disk, part of a real workflow.
Daily standup hits all three. That’s why I chose it as the first “real” agent in the series.
What the Agent Does
In the base version (Part 4), the flow is straightforward:
- Read today’s raw work notes from
work_logs/2025-05-25.txt - Transform them into a polished standup report
- Save the report to
standups/with a timestamp - Keep a historical record of every standup, queryable on demand
You write bullet points. The agent writes the standup. Four tools, ~150 lines, zero repetitive thinking required.
The extended version (Part 5, the production agent) adds Jira integration and team posting — but the architecture is identical. More on that later.
The Four Tools
The entire agent is built on four @tool functions:
get_current_time — returns the current datetime. Used to name the work log file to read and timestamp the saved standup. Reused from Part 3.
read_work_log — reads work_logs/YYYY-MM-DD.txt. If no file exists, it returns a helpful message explaining what to create. Handles the case where you forgot to write notes: rather than crashing, it guides you.
save_standup — writes the generated standup to standups/standup_YYYY-MM-DD_HH-MM.txt. Timestamped, UTF-8, permanent. Every standup you’ve ever generated is on disk.
list_standups — lists all saved standup files, newest first. This is the cross-day memory: you can ask “show me my past standups” and the agent reads history from disk without any vector database or embedding nonsense.
@tool
def read_work_log(date: str = "") -> str:
"""Reads today's (or a specific date's) raw work notes from the work_logs folder.
Input: date in YYYY-MM-DD format, or empty string for today.
Output: the raw notes text, or a helpful message if none found."""
if not date:
date = datetime.now().strftime("%Y-%m-%d")
filepath = f"work_logs/{date}.txt"
if not os.path.exists(filepath):
return (
f"No work log found for {date}.\n"
f"Create the file: {filepath}\n"
f"Write bullet points of what you worked on, blockers, and plans."
)
with open(filepath, "r") as f:
content = f.read().strip()
return f"=== Work Log for {date} ===\n{content}"
Notice the docstring. It’s not just documentation — the LLM reads it to decide when and how to call the tool. Good docstrings are the difference between a smart agent and a confused one.
The Standup Format
The agent writes every report in the same four-section structure:
✅ Done — what was completed (past tense, clear outcomes)
🚧 In Progress — what is still being worked on
⚠️ Blockers — anything blocking progress (or "None" if clear)
📅 Plan For Tomorrow — next priorities based on current status
This is enforced in the system prompt, not in the tools. The tools are just file I/O — the reasoning and writing happen in the LLM.
The Agent Loop: Identical to Parts 1–3
If you’ve followed the series, there’s nothing new to learn here. The agent loop is the same pattern as always:
def ai_agent_interaction(user_input: str) -> str:
messages.append(HumanMessage(content=user_input))
for _ in range(10): # max 10 iterations
response = llm_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls: # final answer
break
for tc in response.tool_calls:
print(f" 🔧 Using tool: {tc['name']}({tc['args']})")
result = tools_by_name[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
return messages[-1].content
When you say "generate my standup for today", here’s what actually happens in the loop:
- Iteration 1: LLM decides to call
get_current_time→ gets today’s date - Iteration 2: LLM calls
read_work_logwith today’s date → gets your raw notes - Iteration 3: LLM writes the standup prose, calls
save_standup→ saved to disk - Iteration 4: LLM returns the final standup text → you see it in the terminal
Four tool calls, zero manual steps. Watch the 🔧 Using tool: lines in your terminal — that’s the agent reasoning in real time.
Your Work Log: What to Write
The agent reads work_logs/YYYY-MM-DD.txt. On first run, it creates a sample file so you can see the format immediately:
- finished implementing user authentication API endpoint
- fixed bug in the payment processing module (null pointer when card expires)
- reviewed PR #47 from Sarah, left 3 comments
- started working on dashboard performance — slow query identified
- blocker: waiting for design team to finalize the new dashboard mockups
- tomorrow: continue dashboard optimization, team sync at 10am
Bullet points. No formatting required. Write exactly what you’d say out loud to a colleague. The agent transforms the raw notes into polished prose — that’s its entire job.
Setup in Three Steps
Install dependencies:
pip install langchain-ollama langchain-core
# and run Ollama locally:
ollama pull qwen3.5:9b
Create your folder structure (the script does this automatically):
your_project/
├── work_logs/ ← you write notes here
├── standups/ ← agent saves reports here
└── daily_standup_generator.py
Run and try these prompts:
• generate my standup for today
• show me my past standups
• what's in my work log for today?
• generate my standup for 2025-05-20
The Production Version: Jira + Team Channels
Part 4 is the foundation. The production agent (Part 5) adds three more tools on top of the same architecture:
fetch_jira_tickets — calls the Jira REST API with JQL (assignee = currentUser() AND status IN ("In Progress", "Done")). Returns ticket keys, summaries, statuses. No manual note-taking at all.
build_work_log_from_jira — formats Jira tickets as bullet points and writes the work log file automatically. You can edit it before the standup is generated if you want to add context.
post_to_google_chat / post_to_slack — HTTP POST to incoming webhook URLs using only Python’s built-in urllib. No third-party libraries. One call posts to your entire team channel.
The pipeline in production mode:
Jira API → fetch_jira_tickets → build_work_log_from_jira
→ read_work_log → LLM writes standup → save_standup
→ post_to_google_chat → post_to_slack
Seven tool calls. One command:
generate my standup from Jira and post to both Google Chat and Slack
And with a cron job:
# Runs automatically every weekday at 5:30pm
30 17 * * 1-5 /path/to/python /path/to/standup_agent.py --auto
You stop thinking about standup entirely.
What I Learned Building This
The system prompt is more important than the tools. The tools are just file reads and API calls. The quality of the standup output — the tone, structure, clarity — comes entirely from how well the system prompt defines the task. I rewrote the system prompt three times before I was happy with the output.
Graceful degradation matters. If the work log doesn’t exist, the agent tells you how to create it rather than crashing. If Jira isn’t configured, it falls back to manual mode. Build for the error cases first.
Cross-day memory from flat files is underrated. I considered SQLite for storing standups. I’m glad I didn’t. Flat .txt files with date-stamped names are readable, searchable with grep, version-controllable, and require zero setup. The list_standups tool is 10 lines. For a personal productivity agent, that’s the right call.
Tool docstrings are agent instructions. The LLM doesn’t have a hard-coded decision tree. It reads the tool descriptions and reasons about what to call next. If a docstring is vague, the agent makes wrong decisions. Treat every docstring as a prompt.
The Pattern That Scales to Everything
Here’s the thing I keep coming back to after building four of these agents in a row:
Real data source → Tools → LLM reasons → Real output
That’s it. Replace the boxes with your problem. The loop in the middle never changes.
For standup: work_logs/ or Jira → 4 tools → Ollama/Gemini → Slack
For GitLab workflow (Part 7): local files → 15 tools → Gemini → GitLab API
For a customer support agent: ticket database → query tools → LLM → reply draft
The same agent loop, the same @tool decorator, the same messages list. Once you internalize this pattern, you can build a new agent for almost any repetitive workflow in an afternoon.
What’s Coming Next
The series continues:
- Part 5: Jira integration + Google Chat + Slack posting — the full production agent
- Part 6: Multi-tool memory and context persistence across sessions
- Part 7: GitLab Developer Workflow Agent — branch, commit, MR, AI review, merge (15 tools)
If you want to follow along, all the code is in the GitLab repo linked in my profile. Each part builds directly on the last.
Try It In Five Minutes
The barrier to running this is genuinely low: Python, Ollama, and pip install langchain-ollama. The agent creates its own folders, generates a sample work log, and you can generate your first standup in under a minute.
The interesting part comes on day two, when you ask "show me my past standups" and the agent reads back your history — across sessions, without any database, because it saved everything to disk.
That’s the moment it stops feeling like a tutorial toy and starts feeling like something you’ll actually use.
Follow for Part 5 — Jira and team notifications turn this into something you never have to think about again. Full code in the GitLab repo linked in my profile.