Most AI agents answer a question, run a tool, and then end the session. But what if you need an agent that remembers your preferences, keeps track of unfinished work, schedules tasks, and picks up where it left off even after restarting your app? That’s what sets a persistent AI agent apart from a basic chatbot. In this tutorial, we’ll build an always-on AI agent with Python and LangGraph. We’ll use local storage, open-source libraries, and Ollama for local LLM inference, so you won’t need to pay for an API or subscription.
Build an Always-On AI Agent
Our agent will have six main components:

We’ll use LangGraph to manage the workflow, SQLite for saving data, APScheduler for scheduling tasks, and Ollama to run an open-source model on your machine.
Install the required tools
Install Ollama from its official website and download a locally supported model, such as Llama 3.2.
Install the Python dependencies:
pip install -U langgraph langchain-ollama \
langchain-core langgraph-checkpoint-sqlite \
apscheduler python-dotenv
Download the model:
ollama pull llama3.2
You can use all these components without purchasing an API subscription.
Create the persistent agent
Create a file called agent.py. We’ll start with a basic system that lets the agent get a goal, make a plan, carry out supported actions, and save its progress:
import sqlite3
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_ollama import ChatOllama
llm = ChatOllama(
model="llama3.2",
temperature=0
)
class AgentState(TypedDict):
goal: str
plan: list
results: list
status: str
def plan_task(state):
prompt = f"""
Create a numbered execution plan for this goal:
{state['goal']}
Return one actionable step per line.
"""
response = llm.invoke(prompt)
steps = [
line.strip("0123456789. -")
for line in response.content.splitlines()
if line.strip()
]
return {"plan": steps, "status": "planned"}
def execute_task(state):
results = []
for step in state["plan"]:
results.append(f"Pending execution: {step}")
return {
"results": results,
"status": "completed"
}
builder = StateGraph(AgentState)
builder.add_node("planner", plan_task)
builder.add_node("executor", execute_task)
builder.add_edge(START, "planner")
builder.add_edge("planner", "executor")
builder.add_edge("executor", END)
connection = sqlite3.connect(
"agent_memory.db",
check_same_thread=False
)
with SqliteSaver(connection) as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
config = {
"configurable": {"thread_id": "user-001"}
}
result = graph.invoke(
{
"goal": "Prepare a weekly AI research report",
"plan": [],
"results": [],
"status": "started"
},
config
)
print(result)This is our starting setup. Right now, the executor just records the steps it plans to take instead of running any action. Next, we’ll add tools and approvals to control what it can do.
Add short-term and long-term memory
An always-on agent needs two different types of memory:
- Short-term memory: Stores the current conversation, active plan, completed steps, and unfinished tasks.
- Long-term memory: Stores information that should remain available across different conversations, such as user preferences, recurring goals, and important facts.
LangGraph provides two useful persistence mechanisms:
- Checkpointers save graph state associated with a thread.
- Stores save application data that can be accessed across threads.
For our project, we can implement simple long-term memory using SQLite:
import sqlite3
db = sqlite3.connect("agent_memory.db")
db.execute("""
CREATE TABLE IF NOT EXISTS memories (
user_id TEXT,
memory_key TEXT,
memory_value TEXT,
PRIMARY KEY(user_id, memory_key)
)
""")
def remember(user_id, key, value):
db.execute("""
INSERT OR REPLACE INTO memories
VALUES (?, ?, ?)
""", (user_id, key, value))
db.commit()
def recall(user_id):
rows = db.execute("""
SELECT memory_key, memory_value
FROM memories WHERE user_id = ?
""", (user_id,)).fetchall()
return dict(rows)
remember("user-001", "preference",
"Prefers Python-based AI projects")
print(recall("user-001"))Now your app can look up saved information before making a new plan.
For example, if a user regularly requests Python tutorials, the agent can use that preference when preparing future learning plans.
In production, make sure to separate memories the user has approved from information the model guesses. Let users see, update, or delete anything that’s been saved.
Give the agent tools to execute tasks
Memory by itself isn’t enough. The agent also needs tools to work with its environment.
Let’s create a small tool registry containing file creation and task tracking:
from pathlib import Path
import json
def create_note(filename, content):
safe_name = Path(filename).name
path = Path("agent_notes") / safe_name
path.parent.mkdir(exist_ok=True)
path.write_text(content)
return f"Created {path}"
def save_task(task, status="pending"):
connection = sqlite3.connect("agent_memory.db")
connection.execute("""
CREATE TABLE IF NOT EXISTS tasks (
task TEXT,
status TEXT
)
""")
connection.execute(
"INSERT INTO tasks VALUES (?, ?)",
(task, status)
)
connection.commit()
connection.close()
TOOLS = {
"create_note": create_note,
"save_task": save_task
}Next, let the LLM select an appropriate tool based on the plan.
For a more advanced setup, use LangChain tools and LangGraph’s patterns for running tools. Always check tool arguments before running them and only allow access to approved functions.
For example, an agent could:
- Receive a request to prepare a research report.
- Create a research task.
- Generate a report outline.
- Save the outline to a file.
- Update the task status.
This kind of workflow is much more helpful than just giving a single response.
Ace the AI Interview
Building agents like this can help your resume stand out, but hiring managers may ask you to explain the architecture on a whiteboard. If you want to feel confident in interviews and handle technical questions smoothly, take a look at my playbook: Cracking Your First AI/ML Interview.
Add recurring task scheduling
An always-on agent should be able to perform tasks at predefined intervals.
For this, we’ll use APScheduler, an open-source Python scheduling library:
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
def daily_agent_task():
print("Starting scheduled AI task")
config = {
"configurable": {
"thread_id": "daily-research"
}
}
result = graph.invoke(
{
"goal": "Prepare today's AI research summary",
"plan": [],
"results": [],
"status": "started"
},
config
)
print(result)
scheduler.add_job(
daily_agent_task,
"cron",
hour=9,
minute=0,
id="daily_research",
replace_existing=True
)
scheduler.start()This example triggers the agent every day at 9 AM, using the machine’s local timezone.
You can extend it to support weekly reports, reminders, recurring research tasks, and scheduled data analysis.
In production, save your scheduled jobs, set the timezone clearly, and make sure jobs don’t run twice if you have more than one worker. Use a reliable job store or an external scheduler if needed.
Add human approvals and error recovery
Not every action should execute automatically.
For example, deleting files, sending emails, modifying databases, or making financial transactions should require explicit approval.
LangGraph supports human-in-the-loop workflows through interrupt():
from langgraph.types import interrupt
def approval_node(state):
decision = interrupt({
"action": "Execute planned operation",
"plan": state["plan"],
"message": "Approve this action?"
})
if decision != "approved":
return {"status": "rejected"}
return {"status": "approved"}With a persistent checkpointer and a thread ID, the workflow can pause and later resume using Command(resume=...).
To handle errors, add retries with increasing wait times, set timeouts, and make sure failures are clear. Make outside operations idempotent so restarting a workflow doesn’t repeat actions that already worked.
Deploy your always-on AI agent
You can start by running the app on your own computer. If you want it to run all the time, try using Docker and a small server.
Create a simple Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "scheduler.py"]Build and run:
docker build -t persistent-ai-agent .
docker run -d \
--name ai-agent \
--restart unless-stopped \
-v ./data:/app/data \
persistent-ai-agent
In production, keep your SQLite files on persistent storage, manage secrets outside the image, and make sure the Ollama service can be reached from the container.
You can eventually replace SQLite with PostgreSQL and use a dedicated job scheduler for larger workloads.
The Takeaway
Building an always-on AI agent is not just about connecting an LLM to a collection of tools. It’s about building a system that can remember useful information, maintain its state, plan work, execute actions safely, and recover when something goes wrong.
I recommend starting with a simple LangGraph workflow and SQLite persistence. Once that works, introduce long-term memory, tool calling, scheduling, and human approvals.
You can explore the LangGraph documentation and its persistence guide to understand these concepts in greater depth.
I hope you liked this article on how to build an always-on AI agent using Python and LangGraph. If you want more tips on AI and machine learning, feel free to follow me on Instagram.





