Build AI Employees in Python That Work While You Sleep

Riya Bansal. Last Updated : 22 Oct, 2026
10 min read

It’s Friday, 8 a.m. Your weekly tech newsletter is drafted, fact-checked and waiting for one click of approval. You didn’t write it. You were asleep.

Three AI employees did. A researcher found the week’s best links. A writer turned them into a draft. An editor caught the writer inventing a statistic, fixed it, and then left you a ticket asking for sign-off before anything went public. That’s exactly the behavior you want from a new hire.

Now compare that to how most of us work with AI today. Thursday night, twelve tabs open. You paste a link into a chatbot, ask for a summary, paste it back, ask for a rewrite, then check whether the facts hold up. The AI is smart, but you’re still the one doing the managing. That’s not delegation. That’s babysitting.

The difference between an assistant and an employee isn’t intelligence. It’s structure. An employee has one clear job, a defined set of tools, rules they can’t break and a manager who approves the risky calls. Give an AI agent the same structure and it can work unsupervised.

In this tutorial, you’ll build AI employees in Python: a three-person newsletter crew that runs overnight. No frameworks, about 150 lines of code. Each employee gets a job contract, a toolbelt limited by agent tool permissions, and an AI approval workflow that keeps you in charge of anything that goes public.

Project Overview

At the conclusion of this tutorial, you will successfully piece together an active crew that consists of the following:

  • Three agents (Scout, Quill and Red) capitalizing on the same LLM engine
  • Guardrails that are implemented in programming so agents will never be able to publish secrets or break the law 
  • Transfer technology whereby each agent is able to pass outputs

Aside from that, you will also make the following installations:

  • Approval desk so that when agents want to initiate high-risk operations, they will need approval from a human
  • Cron scheduling so that your crew does its work even while you are sleeping

Here’s a sample message that Red leaves you on a Friday morning:

sample message by an AI Employee

The editing agent apprehended the writing agent fabricating information. It then went ahead to rectify the document. Thereafter, it consulted you before proceeding to publicize the document. The conduct exhibited is what we need.

This is meant for developers with a basic knowledge of Python and who have perhaps used an LLM API once or twice. You may use an OpenAI API key or even rely on free local models to follow.

Project Architecture: How Your AI Employees Work Together

Before writing any code, let’s look at how this team is organized. Think of it as a small company with three employees and one manager.

  • The engine is shared. Every employee runs on the same LLM with the same configuration. You pick one model, and the whole team uses it. Switching models later changes the whole team at once.
  • Each employee has one job. Scout researches, Quill writes and Red edits. Each one gets a written job contract, a fixed set of tools and a folder they’re allowed to write in. Nothing more.
  • You are the manager. No employee can publish, send or delete anything. When Red thinks the newsletter is ready, it doesn’t hit publish. It files an approval ticket and waits for you.
  • Work moves through files, not conversations. The employees never talk to each other. Each one finishes its task, leaves a file in a shared folder and clocks out. The next employee picks up that file and starts their shift:
Project Architecture: How Your AI Employees Work Together

Here’s what each employee is allowed to do. The “Can’t” column isn’t a polite request. In Step 1, you’ll enforce it in code:

AI Employee roles

Why Not Just Use Framework? 

Frameworks are useful once you know what you need. Until then, they hide the parts you should understand first. When does the loop stop? Who checks permissions? What happens when a website goes down? Write these 150 lines yourself, and the answers become obvious. When you do move to a framework, you’ll pick the one you actually need, not the one with the most popular launch video.

Getting Started with the Project 

For this task, Python 3.10 or a later version is required along with a model capable of tool calling. You can use any OpenAI-compatible endpoint including a local model that you can use for free.  

kdir night-shift && cd night-shift  

python3 -m venv .venv && source .venv/bin/activate  

pip install "openai>=1.40"  

mkdir roles memory inbox 

Now you create .env. Choose one engine that you can use throughout your operations.  

# Option A: OpenAI  

OPENAI_API_KEY=sk-...  

MODEL=gpt-4.1-mini  

# Option B: free and local with Ollama (run: ollama pull qwen2.5:7b)  

# OPENAI_BASE_URL=http://localhost:11434/v1  

# OPENAI_API_KEY=ollama  

# MODEL=qwen2.5:7b  

MAX_STEPS=15 

The OpenAI SDK picks up OPENAI_API_KEY and OPENAI_BASE_URL automatically, and shift.py reads MODEL and MAX_STEPS. To switch engines later, you only edit .env.

Step 1: The Rulebook Is Code, not a Pep Talk 

Some tutorials give instructions in their prompts, “Don’t ever publish.” That’s just a pep talk. Models are obedient most of the time. “Most of the time” is not very good at 3 a.m.  

Here’s the version you’ll find everywhere: 

# Before: the "rule" is a polite sentence the model may ignore  

SYSTEM = "You are a writer. Please do not publish or send emails."  

tools = [read_file, write_file, publish_post, send_email] # all of them!

And here is ours, called roster.py: 

# After: tools and write paths are enforced by code, not by vibes  

ROSTER = {  

"scout": {  

"tools": ["read_file", "fetch_url", "write_file"],  

"writes": ["handoffs/"],  

"done": "handoffs/{today}/links.md",  

},  

"quill": {  

"tools": ["read_file", "write_file"],  

"writes": ["drafts/"],  

"done": "drafts/{today}/issue.md",  

},  

"red": {  

"tools": ["read_file", "write_file", "request_approval"],  

"writes": ["out/"],  

"done": "out/{today}/review.md",  

},  

} 

Take a close look. There’s no publishing functionality anywhere in the room. Thus, Red can only ask, since you cannot misuse the tools, you were never granted to begin with. 

Step 2: Furnish the Office 

Several tools are required for every office. One should not overdo with the tools of the office. Save it as tools.py: 

 import json, re, time, pathlib, urllib.request  

ROOT = pathlib.Path(__file__).parent.resolve()  

def _safe(path):  

p = (ROOT / path).resolve()  

if ROOT not in p.parents:  

raise PermissionError(f"{path} is outside the office")  

if p.name.startswith(".env"):  

raise PermissionError("secrets are off limits")  

return p  

def read_file(path):  

return _safe(path).read_text()[:8000]  

def write_file(path, content, allowed):  

p = _safe(path)  

rel = p.relative_to(ROOT).as_posix() # kills "handoffs/../out" tricks  

if not any(rel.startswith(prefix) for prefix in allowed):  

return f"DENIED: you may only write under {allowed}"  

p.parent.mkdir(parents=True, exist_ok=True)  

p.write_text(content)  

return f"wrote {len(content)} chars to {path}"

The _safe() function can be referred to as the office door. Agents cannot get outside the project directory. Also, they cannot read .env. An agent which will come across the API key will inevitably leak it.

Don’t forget to add the final two tools into this file:

def fetch_url(url):  

req = urllib.request.Request(url, headers={"User-Agent": "night-shift/0.1"})  

html = urllib.request.urlopen(req, timeout=15).read().decode("utf-8", "ignore")  

html = re.sub(r"<(script|style).*?</\1>", "", html, flags=re.S)  

text = re.sub(r"<[^>]+>", " ", html)  

return re.sub(r"\s+", " ", text)[:4000]  

def request_approval(action, summary, artifact):  

# The fence: risky verbs become a ticket, never an action.  

ticket = {"action": action, "summary": summary,  

"artifact": artifact, "asked_at": time.ctime()}  

pending = ROOT / "pending"  

pending.mkdir(exist_ok=True)  

path = pending / f"{int(time.time())}-{action}.json"  

path.write_text(json.dumps(ticket, indent=2))  

return f"queued for human approval: {path.name}" 

The function request_approval is the most important one in the article. The function is not supposed to publish anything. It creates a ticket and waits for a human being. 

Step 3: Print the Tool Menu 

The model is unable to process your Python code. All it can read is the menu made in JSON format. Save it as schemas.py file: 

def _fn(name, desc, **props):  

return {"type": "function", "function": {  

"name": name, "description": desc,  

"parameters": {"type": "object", "required": list(props),  

"properties": {k: {"type": "string", "description": v}  

for k, v in props.items()}}}}  

SCHEMAS = {  

"read_file": _fn("read_file", "Read a file in the workspace.",  

path="Relative path"),  

"write_file": _fn("write_file", "Create or overwrite a file.",  

path="Relative path", content="Full file text"),  

"fetch_url": _fn("fetch_url", "Fetch a web page as plain text.",  

url="Full https URL"),  

"request_approval": _fn("request_approval",  

"Ask the human to approve a risky action like publish or send.",  

action="publish | send | spend | delete",  

summary="One line on what and why", artifact="Path to the file"),  

} 

Each agent can only find the tools that it has in its own toolbox. Quill’s toolbox has two menu items only. 

Step 4: Sign the Employment Contracts 

A charter refers to a job contract. It is created once and does not require updates. Its purpose is to answer five questions: output, inputs, limits, outcomes, and checkpoints. A charter from Scout is as follows: 

 # Scout: research desk  

Outcome: a shortlist of this week's best links for the newsletter.  

Sources: only the URLs in inbox/sources.txt. Read that file first.  

Constraints: fetch each URL once. Skip pages that fail. Max 5 picks.  

Deliverable: write handoffs/{today}/links.md. For each pick add: title, URL, a two-line summary, and why our readers should care. If a source failed, list it under "## Gaps". Never invent a link.  

Stop as soon as the file is written. 

Quill and Red will fill in the same template. Quill writes 400 words based on the contents of the Scout list. Red checks each hyperlink, fixes any issues, and obtains the necessary approvals. 

I specifically prefer the boring part of the charter because Quill’s contract mentioned what to do in case Scout’s list is blank. Hence, when encountering an empty Scout file, Quill would write an enjoyable newsletter about nothing! 

 - Newsletter name: The Friday Byte. Audience: working data folks.  

- Voice: short sentences, no hype words, one joke max per issue. 

- Never use the words "revolutionary" or "game-changing".  

- Sign-off is always: "See you next Friday. Ship something small."

Shared memory would contain these facts about the company that require familiarity from every agent. 

Step 5: Clocking In (The Agent Loop) 

Here lies the engine room. It’s time to create a shift.py and begin with the setup: 

import json, os, sys, datetime  

from openai import OpenAI  

import tools  

from roster import ROSTER  

from schemas import SCHEMAS  

client = OpenAI() # reads OPENAI_API_KEY and OPENAI_BASE_URL  

MODEL = os.environ["MODEL"] # one engine for the whole company  

MAX_STEPS = int(os.environ.get("MAX_STEPS", 15))  

TODAY = datetime.date.today().isoformat()  

def brief_for(name):  

charter = (tools.ROOT / "roles" / f"{name}.md").read_text()  

shared = (tools.ROOT / "memory" / "shared.md").read_text()  

return charter.replace("{today}", TODAY) + "\n\n# Shared memory\n" + shared 

Every agent receives its own contract as well as a company memory, the same stove, but different places.  

Next comes the guardsman, every tool call goes through it: 

def dispatch(name, call):  

role = ROSTER[name]  

tool = call.function.name  

args = json.loads(call.function.arguments or "{}")  

if tool not in role["tools"]:  

return f"DENIED: {tool} is not on your toolbelt" # fence, in code  

if tool == "write_file":  

args["allowed"] = [p.replace("{today}", TODAY) for p in role["writes"]]  

try:  

return getattr(tools, tool)(**args)  

except Exception as e:  

return f"ERROR: {e}" # the agent sees it and can adapt

Errors return to the model in plain text. Dead sites don’t spoil the evening. Scout recognizes the error, sees what is missing, and continues working. 

Now, the loop that turns it into an agent:  

 def run_agent(name):  

role = ROSTER[name]  

messages = [{"role": "system", "content": brief_for(name)},  

{"role": "user", "content": "Start your shift."}]  

toolbelt = [SCHEMAS[t] for t in role["tools"]]  

for step in range(1, MAX_STEPS + 1):  

msg = client.chat.completions.create(  

model=MODEL, messages=messages, tools=toolbelt  

).choices[0].message  

messages.append(msg.model_dump(exclude_none=True))  

if not msg.tool_calls:  

break # the agent says it is done  

for call in msg.tool_calls:  

result = dispatch(name, call)  

print(f"[{name}] {step}: {call.function.name} -> {result[:60]!r}")  

messages.append({"role": "tool", "tool_call_id": call.id,  

"content": result})  

return check_done(name) 

The model thinks, calls the tool, reads the result and continues doing the same thing again and again. MAX_STEPS is the limit to overtime. Without the limit, a disoriented agent may run the process overnight at the expense of your credit card. 

Step 6: No File, No Paycheck 

This is the principle that is responsible for transforming a chatbot into an employee. Saying “Done!” does not carry much weight for an agent. An existing file is all that matters.

def check_done(name):  

# Judge the shift by the folder, not the chat.  

done = tools.ROOT / ROSTER[name]["done"].replace("{today}", TODAY)  

status = "DONE" if done.exists() else "STOPPED, UNFINISHED"  

log = tools.ROOT / "out" / TODAY / "shift-log.md"  

log.parent.mkdir(parents=True, exist_ok=True)  

with log.open("a") as f:  

f.write(f"- {datetime.datetime.now():%H:%M} {name}: {status}\n")  

print(f"[{name}] {status}")  

return status == "DONE"  

if __name__ == "__main__":  

crew = sys.argv[1:] or ["scout", "quill", "red"]  

for name in crew:  

if not run_agent(name):  

sys.exit(f"Handoff broken at {name}. Read the log.") 

If the Scout flicks the baton, the Quill will never be able to run. The chain is broken. 

Opening Night: Run the Full Demo 

Treat the initial run like a job interview. You should be present during the live operation so that you can witness at least one successful finish before believing your closed laptop.  

set -a; source .env; set +a  

python shift.py

Your terminal should appear something like this:

Now, look at the folder. You should have all the information stored in it:

This is my test for every run with an agent. Would a stranger who finds this folder be able to deduce what happened? If the answer is yes, then you have hired a good agent.

The Break-In Test: Try to Corrupt Your Own Crew 

The guidelines that don’t get tested only serve as an ornament. Let’s take on the role of the bad guy for five minutes. 

Upload a sneaky line to roles/quill.md one line at a time and execute python shift.py quill after each one. After running the command, delete the line.

Some Patterns will get trapped by this approach while some will avoid getting tested by themselves. Still, this is just a difference between a rule and a desire. 

Step 7: Run the Pass (Your Approval Desk) 

Only quick checking of ticket works in favor of ticket. Save this as approve.py:  

import json, pathlib, shutil  

ROOT = pathlib.Path(__file__).parent  

for ticket in sorted((ROOT / "pending").glob("*.json")):  

t = json.loads(ticket.read_text())  

print(f"\n{t['action'].upper()}: {t['summary']}\nFile: {t['artifact']}")  

answer = input("Approve? [y/N] ").strip().lower()  

dest = ROOT / ("approved" if answer == "y" else "rejected")  

dest.mkdir(exist_ok=True)  

shutil.move(ticket, dest / ticket.name)  

print(f"-> moved to {dest.name}/") 

The Friday morning now is two minutes long:

Approving ticket files. You still copy the ticket issue to your email tool. This continues purpose, and publishing remains a job for an actual human until the team gets promoted.

Also Read: AgentScope AI: A Complete Guide to Building Scalable Multi-Agent Systems with LLMs

Your Turn: Three Upgrades to Try 

Eager to go on? These items are all simple and can be done in under sixty minutes: 

  • Send out a Red email to yourself alerting you of the approved problem. Every time you perform this action, log it.  
  • Employ Librarian, this is a fourth agent that will refresh your memory/shared.md file with the topics used. No repeats of topics will follow.  
  • Install a budget meter. Check every response for its total tokens used in order to stop the work should the limit go way over.  

Every promotion works the same. A new verb is acquired only after some clean weeks. 

Conclusion 

You just built AI employees in Python. They share one engine, but each has a single job, a limited set of tools and rules enforced in code rather than in prompts. None of them can publish anything without your sign-off.

The most important habit you’ve picked up isn’t in any one file. It’s how you judge the work. You don’t trust an employee because it says “Done!” You trust it because the file is in the folder.

So start small. Give Scout three links tonight, run the shift and close your laptop. If links.md is waiting for you in the morning, congratulations. You’re officially a manager.

Gen AI Intern at Analytics Vidhya 
Department of Computer Science, Vellore Institute of Technology, Vellore, India 

I am currently working as a Gen AI Intern at Analytics Vidhya, where I contribute to innovative AI-driven solutions that empower businesses to leverage data effectively. As a final-year Computer Science student at Vellore Institute of Technology, I bring a solid foundation in software development, data analytics, and machine learning to my role. 

Feel free to connect with me at [email protected] 

Login to continue reading and enjoy expert-curated content.

Responses From Readers

Clear