Planning Agent: How I Built AI That Sequences Tasks

There is a difference between a to-do list and a project plan. A to-do list tells you what needs to happen. A project plan tells you what needs to happen first.


What This Agent Does

The Planning Agent accepts a high-level goal and breaks it into a structured, dependency-ordered sequence of tasks. It does not just list what needs to happen. It figures out which tasks depend on which others and produces an execution sequence where every step runs only after its prerequisites are complete.

When I gave it the goal of launching a client onboarding workflow for NosisTech LLC, the verified run with gemini-flash returned a 17-step execution order. It began with understanding NosisTech’s needs and identifying stakeholders, then moved through service agreement review, onboarding plan design, billing setup, CRM/project access, kickoff, training, monitoring, post-onboarding review, and final launch.


Why Build This

Most AI agent demonstrations show a system that answers questions or summarizes documents. Those are useful. But the moment you ask an agent to accomplish something that requires multiple steps in the right order, most systems fall apart. They either produce a flat list with no regard for sequence, or they hallucinate dependencies that do not exist.

I wanted to understand whether a pure Python agent, with no orchestration framework, could produce a genuinely executable project plan. Not a formatted response. An actual dependency graph that could drive real execution. The answer turned out to be yes, and the architecture behind it is simpler than most people expect.


What Happened

What happened: I simplified agent.py from 187 lines to 123 lines while keeping the core planning behavior: ask the model for JSON tasks, validate dependencies, sort the plan, and print the execution order.

What broke: The first simplified run produced an undefined dependency. The validator caught it immediately instead of accepting a broken plan. I tightened the prompt so every depends_on value must exactly match another task field in the JSON array. After that, the verified run succeeded.

What surprised me: The useful part was not the number of tasks. It was the validation layer. The model can propose a plan, but the Python code decides whether that plan is structurally executable.

What I would change next: the current build prints a valid execution order, but it does not yet execute real work. A future version could group independent tasks and run them in parallel with asyncio before handing completed steps to real API calls, database writes, notifications, or project management tools.

Why this matters: The useful part is not trusting the model blindly. The model proposes the plan, but the Python code validates that every dependency exists and that the final sequence can actually run. That is what turns an AI-generated task list into something closer to an executable project plan.


The Architecture: How It Works

The Planning Agent has four stages that run in sequence. Understanding each one is the key to understanding why this approach is more reliable than asking an AI to just describe what to do.

Stage One: Goal to Task List

The agent sends the goal to the LLM with a specific instruction: return only a JSON array where each element has a task name and a list of tasks it depends on. No prose. No explanation. Just structured data. This constraint is intentional. The agent is not asking the model to think out loud. It is asking for machine-readable output it can process in the next stage.

Stage Two: Building the DAG

DAG stands for directed acyclic graph. The concept is simple. Think of it as a map where each task is a location and the arrows between them show which direction you must travel. The arrows only go one way, meaning you never travel in a circle. You cannot arrive at a destination before you have passed through all the required checkpoints before it.

I used these resources to understand DAGs better: risingwave.com and geeksforgeeks.org

The agent takes the task list and converts it into this map. Each task becomes a node. Each dependency becomes a directed arrow. Before the map is finalized, the agent validates that every dependency referenced actually exists in the task list. If the model invented a dependency that points to a task that was never defined, the agent catches it immediately and exits with a clear error message.

Stage Three: Topological Sort

Once the map exists, the agent needs to determine the correct order to visit every location. This is called a topological sort. The algorithm the agent uses is called Kahn’s algorithm.

The idea works like this. Start by finding every task that has no dependencies, meaning no arrows point into it. These tasks can start immediately. Process one of them, mark it complete, and remove it from the map. Now check whether removing that task has freed up any other tasks that were waiting on it. If so, add those to the ready queue. Repeat until every task has been processed. If you finish and some tasks are still unprocessed, it means there was a circular dependency. Task A was waiting for Task B, and Task B was waiting for Task A, and neither could ever start. The agent detects this and exits with a clear message rather than hanging indefinitely.

Imagine you are getting ready in the morning. You cannot put on your shoes before your socks. You cannot put on your socks before you get out of bed. There is a required order. Some things must happen before others. That is all Kahn’s algorithm does. It looks at a list of tasks where some tasks depend on others, and it figures out the safe order to do them all.

Here is how it thinks:

Step 1. Look at every task. Find the ones that have nothing blocking them, meaning no other task needs to finish first. Those go first. In the morning example, getting out of bed goes first because nothing blocks it.

Step 2. Do one of those unblocked tasks. Cross it off the list.

Step 3. Now that you crossed it off, check if crossing it off unblocked anything else. If putting on socks was the only thing blocking shoes, then shoes just became available. Add shoes to the ready list.

Step 4. Repeat until everything is done.

The one safety check. If you finish and some tasks never became unblocked, it means two tasks were waiting on each other and neither could ever start. The algorithm catches that and says: this plan is impossible, there is a circular dependency.

Stage Four: Execution Order

The agent prints the sorted task list in execution order. In this teaching version, it does not execute external actions. The architecture is the lesson. The execution layer is where real API calls, database writes, notifications, or external system integrations would plug in later.

The Model-Agnostic Layer

Every LLM call routes through LiteLLM. The agent has no knowledge of which provider is running. It sends a standard request and receives a standard response. Switching from DeepSeek V4 Pro to Gemini Flash required changing one line in the .env file. No code changed between the two provider tests. Both produced valid, correctly ordered plans.


The Build: Step by Step

Environment Validation

def require_config():
    required = ["LITELLM_BASE_URL", "MODEL_NAME", "LITELLM_API_KEY", "GOAL"]
    missing = [key for key in required if not os.getenv(key)]
    if missing:
        raise SystemExit("Missing required environment variables: " + ", ".join(missing))
    if len(os.getenv("GOAL")) > 1000:
        raise SystemExit("The GOAL exceeds 1000 characters. Please shorten it.")

What This Code Is Actually Doing

Before the agent makes a single API call, it checks that every required configuration value is present. Think of this as the agent checking that it has its keys, wallet, and phone before leaving the house. If anything is missing, it tells you exactly which items are missing and stops. It does not attempt to run with incomplete configuration and fail halfway through with a confusing error. The exit happens at the front door, not in the middle of the journey.


Asking the Model for a Plan

def ask_model(client, model, goal):
    messages = [
        {
            "role": "system",
            "content": (
                "Break the goal into dependency-aware tasks. Return only JSON. "
                "Use an array of objects with task and depends_on fields. "
                "Every depends_on value must exactly match another task field in the array."
            ),
        },
        {"role": "user", "content": f"Goal: {goal}"},
    ]
    for attempt in range(1, 4):
        try:
            response = client.chat.completions.create(
                model=model,
                messages=messages,
                temperature=0,
            )
            return response.choices[0].message.content
        except RateLimitError:
            wait = 2 ** attempt
            print(f"Rate limit hit. Retrying in {wait} seconds.")
            time.sleep(wait)
        except APIConnectionError as error:
            raise RuntimeError("Cannot reach LiteLLM. Check the proxy.") from error
        except OpenAIError as error:
            raise RuntimeError(f"LiteLLM request failed: {error}") from error
    raise RuntimeError("Failed after multiple rate-limit retries.")

What This Code Is Actually Doing

This function does two things. First, it sends the goal to the LLM with precise instructions about what format to return. The system prompt is deliberately restrictive: return only a JSON array, no prose, no markdown. This is like sending someone to buy groceries with a list that says exactly what to bring back and nothing else.

Second, it handles common API failure points. If the model provider rate-limits the request, the function waits and retries. If the LiteLLM proxy cannot be reached, it raises a clear error message. If the provider returns another OpenAI-compatible API error, the function reports that the LiteLLM request failed. This keeps the model call small while still making failures understandable.


Parsing and Validating the JSON

def parse_tasks(raw_text):
    text = raw_text.strip()
    if text.startswith("```"):
        text = "\n".join(text.splitlines()[1:])
    if text.endswith("```"):
        text = "\n".join(text.splitlines()[:-1])

    tasks = json.loads(text.strip())
    if not isinstance(tasks, list) or not tasks:
        raise ValueError("Plan must be a non-empty JSON array.")
    for item in tasks:
        if not isinstance(item, dict) or not isinstance(item.get("task"), str) or not isinstance(item.get("depends_on"), list):
            raise ValueError("Each item must include task and depends_on.")
    return tasks

What This Code Is Actually Doing

This function converts the flat task list into a map of relationships. For each task, it records which other tasks must be completed before it can begin. Before recording any relationship, it checks that the referenced task actually exists. This is the equivalent of proofreading a project plan before distributing it to the team. If task BThis function cleans and validates the model output before the agent trusts it.

Models sometimes wrap JSON inside markdown code fences, even when instructed not to. The function strips those fences first. Then it parses the remaining text as JSON and checks that the result is a non-empty list.

Each item must be a dictionary with a task name and a depends_on list. If the model returns prose, malformed JSON, an empty plan, or the wrong structure, the agent rejects it before any ordering logic runs. says it depends on task C, but task C was never defined, the plan is internally inconsistent and cannot be executed safely. The agent catches this before any execution begins rather than failing partway through.


Ordering the Tasks

def order_tasks(tasks):
    names = {item["task"] for item in tasks}
    graph = {name: [] for name in names}
    in_degree = {name: 0 for name in names}

    for item in tasks:
        for dependency in item["depends_on"]:
            if dependency not in names:
                raise ValueError(f"Undefined dependency: {dependency}")
            graph[dependency].append(item["task"])
            in_degree[item["task"]] += 1

    ready = deque([task for task, count in in_degree.items() if count == 0])
    ordered = []
    while ready:
        task = ready.popleft()
        ordered.append(task)
        for next_task in graph[task]:
            in_degree[next_task] -= 1
            if in_degree[next_task] == 0:
                ready.append(next_task)

    if len(ordered) != len(tasks):
        raise ValueError("Cycle detected in task dependencies.")
    return ordered

What This Code Is Actually Doing

This function answers the question: given this map of task dependencies, what is the safe order to execute everything?

It starts by counting how many tasks are blocking each task. A task with zero blockers can start immediately. The algorithm processes one unblocked task at a time, marks it complete, and then reduces the blocker count for every task that was waiting on it. When a task reaches zero blockers it joins the ready queue. This continues until every task is processed.

The final check is the safety net. If the sorted list has fewer tasks than the original map, it means the algorithm got stuck. Two tasks were blocking each other in a circle and neither could ever become unblocked. This is called a cycle, and it makes the plan impossible to execute. The agent detects this and exits with a clear message rather than running forever.


Errors I Hit During This Build

Error: Invalid model name

The error message said: Invalid model name passed in model=deepseek-v4-proge. The .env file had a typo in the MODEL_NAME value. Fixed by correcting the value to the right model string. The environment variable check does not validate the model name format, only that the variable is present. A typo passes the check but fails at the first API call.

Dependency note

Tested with Python 3.11:
openai==2.30.0
python-dotenv==1.1.1

What This Means

Every organization that deploys AI agents will eventually need agents that manage multi-step work. Single-turn question answering is the beginning. The real value is in agents that can take a goal and figure out the path to achieve it without being told each step in advance.

The planning pattern in this build applies directly to any workflow that has dependencies. Onboarding a new client has dependencies. Launching a product has dependencies. Running a compliance audit has dependencies. Any process where step B cannot begin until step A is finished is a candidate for this architecture.

What separates this build from a simple task list generator is the dependency enforcement. The agent does not just name the tasks. It builds a structure that makes out-of-order execution impossible. You cannot configure the automation before the workflow is designed. You cannot launch before the team is trained. The ordering is enforced by the algorithm, not by a human reviewing a document and hoping the team follows the sequence.

This means an AI system that can be given a business objective and return a structured, sequenced plan that an execution layer can act on. The plan is not a suggestion. It is a dependency-validated sequence that can drive real automation.


Tools Used


(c) 2026 NosisTech LLC. Licensed under CC BY 4.0. Use freely, just credit us.

WEEKLY BUILD NOTES

One documented agent build in your inbox, every week

Real systems, real code, the errors left in. No spam, unsubscribe anytime.