MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

JAVA AWT AND HOW IT'S USED

2026-03-17 04:08:22

1.Introduction

Java Absract Window Toolkit(AWT) is a Graphical User Interface(GUI) library.It is used to create windows,buttons,textfields,labels,menus and other components for desktop applications.AWT provides a set of classes that allow programmers to design applications that users can interact with through a graphical interface instead of typing commands.It was one of the earliest GUI toolkits in Java and is found in the java.awt package.

2.Features of Java AWT

Java AWT has several important features:

  • Platform Independance-Java programs that use AWT can run on any OS.
  • GUI components-Provides textfields,labels,buttons,menus and may more components.
  • Event handling-Allows programs to respond to user actions ie when user clicks a button a response is given to the action.
  • Layout management-Helps organise components inside a window(there are different types of layout managers).

3.AWT Components

Components are the different visual elements that appear on the screen and they include:

  • Button-Perfoms an action when clicked
  • Label-Displays text
  • TextField-Allows user to enter text
  • Checkbox-Allows selection of options
  • Frame-Main window container

Example Code:
JButton btnLogin=new JButton();//creates empty button
btnLogin.setText("Login");//inside the button there should be a login text

4.Containers in AWT

These are components that hold other components.They help in structuring the interface and controling how components appear within a window
Examples include:
1. Frame
A Frame is the main top-level window of an application. It contains the title bar, borders, and control buttons such as minimize, maximize and close.
2. Panel
A Panel is a simple container used to group related components together within another container. Panels are often placed inside a Frame to organize elements in sections, making the interface easier to design and manage.
3. Dialog
A Dialog is a pop-up window used to display messages or request input from the user. Dialog boxes are commonly used for alerts, confirmations or data entry.

**5.Layout Managers

They are used to organise components in a container.
Common layout managers include:

- BorderLayout-Divides a screen into 5 regions.
- GridLayout-Arranges components in a screen into rows and columns.
- FlowLayout-Places components in a row from left to right.
- CardLayout-Allows switching between panel
s

## 6.Event Handling in AWT
Event handling allows the program to respond to user actions.
Examples of events:

  • Clicking a button
  • Typing in a text field
  • Selecting a checkbox

Example:

  • ActionListener
  • MouseListener
  • KeyListener

## 7. Example of a Simple AWT Program
import java.awt.*;

public class SimpleAWT {
public static void main(String[] args) {
Frame frame = new Frame("My First AWT Program");

       Button button = new Button("Click Me");
       frame.add(button);
       frame.setBackground(Color.CYAN);

       frame.setSize(300,200);
       frame.setLayout(new FlowLayout());
       frame.setVisible(true);
}

}

NB:Ensure that SimpleAWT is also the filename incase you are using netbeans

A picture of the expected output:

This program creates:

  • A window
  • A button inside the window
  • A background that is Cyan in color

COLORS: Inspired By Anitta | A COLORS INTERVIEW

2026-03-17 04:07:09

Anitta spilled the tea to COLORS about her new jam, "Pinterest," revealing it’s all about swapping the chase for glory with a hunt for pure joy. Apparently, the Pinterest platform itself practically designed the song's imagery!

She even gave a little sneak peek into the creative vision for her upcoming album, 'Equilibrium,' so get ready for some fresh vibes from the artist.

Watch on YouTube

Workflow Automation vs AI Agents: A Developer's Guide

2026-03-17 04:03:47

Your Slack bot that posts a message when a GitHub issue opens is not an AI agent. Your n8n flow that summarizes emails with GPT is not an AI agent either -- it is a workflow with an LLM step bolted on.

The term "AI agent" now means five different things depending on who is selling you something. Zapier calls their Zaps "agents." Lindy calls their workflow chains "agents." LangChain uses the word for autonomous reasoning loops. Research papers use it for systems that perceive, plan, and act.

This confusion is not just semantic. It causes teams to pick the wrong architecture, overpay for LLM calls on tasks that need a simple if/else, or build brittle rule chains for problems that actually require reasoning. This guide draws a clear line between workflow automation and AI agents, shows you both architectures in Python, and gives you a decision framework for when to use which.

The Three Architectures You Are Actually Choosing Between

Forget the marketing terms. In practice, you are choosing between three architectures:

Workflow automation is a trigger followed by a fixed chain of deterministic steps. When event X happens, run Step A, then Step B, then Step C. The execution path is defined at design time. The runtime just follows it. No LLM, no reasoning, no ambiguity.

AI-enhanced workflow is the same fixed chain, but one or two steps call an LLM. The workflow still follows a predetermined path -- the LLM just makes a specific step smarter (classify this ticket, summarize this email). The LLM does not decide what happens next.

Autonomous agent is a goal plus a reasoning loop. You give the system a goal and a set of tools. It decides what to do, observes the result, and decides what to do next. The execution path is determined at runtime, not design time.

Most production systems marketed as "AI agents" are actually AI-enhanced workflows. That is fine -- but calling them agents leads to wrong expectations about what they can handle.

Anatomy of a Workflow: Trigger, Chain, Done

A workflow automation handles a well-defined task with predictable inputs. Every execution follows the same path. Here is a GitHub issue triage workflow that classifies priority and routes to the right Slack channel:

def triage_github_issue(webhook_payload: dict) -> dict:
    """Workflow automation: trigger -> fixed chain of actions."""
    # Step 1: Extract fields (deterministic)
    title = webhook_payload["issue"]["title"]
    body = webhook_payload["issue"]["body"] or ""
    labels = [l["name"] for l in webhook_payload["issue"]["labels"]]

    # Step 2: Classify priority (rule-based, no LLM)
    if "critical" in labels or "production" in title.lower():
        priority, team = "P0", "on-call"
    elif "bug" in labels:
        priority, team = "P1", "engineering"
    elif "feature" in labels:
        priority, team = "P2", "product"
    else:
        priority, team = "P3", "triage"

    # Step 3: Route to Slack (fixed destination)
    post_to_slack(
        channel=team,
        message=f"[{priority}] {title} -- assigned to #{team}"
    )

    return {"priority": priority, "team": team, "routed": True}

This runs in milliseconds. It costs nothing beyond the API calls. It is completely predictable -- the same input always produces the same output. You can test it with unit tests and debug it by reading the code.

Workflow automation wins when:

  • The task is well-defined with clear rules
  • Inputs are structured and predictable
  • Latency and cost matter
  • You need a complete audit trail
  • The logic rarely changes

The limitation is obvious: this workflow cannot handle anything outside its rules. An issue titled "Users report intermittent 500 errors on the payments endpoint" with no labels gets classified P3 and sent to triage. A human would immediately recognize that as a P0.

Anatomy of an Agent: Goal, Reason, Act, Repeat

An autonomous agent handles the same trigger but reasons about what to do. Instead of following a fixed chain, it investigates:

from openai import OpenAI
import json

client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_similar_issues",
            "description": "Search for similar or duplicate GitHub issues",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Search query"}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "check_error_logs",
            "description": "Check recent error logs for a service",
            "parameters": {
                "type": "object",
                "properties": {
                    "service": {"type": "string"},
                    "hours": {"type": "integer", "default": 24}
                },
                "required": ["service"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "post_to_slack",
            "description": "Post a message to a Slack channel",
            "parameters": {
                "type": "object",
                "properties": {
                    "channel": {"type": "string"},
                    "message": {"type": "string"}
                },
                "required": ["channel", "message"]
            }
        }
    }
]

def investigate_issue(issue: dict) -> dict:
    """Autonomous agent: goal -> reasoning loop -> dynamic actions."""
    messages = [
        {"role": "system", "content": (
            "You are a senior engineer triaging a GitHub issue. "
            "Investigate it: check for duplicates, look at error logs "
            "if relevant, determine severity, and route to the right team. "
            "Use the tools available to you."
        )},
        {"role": "user", "content": (
            f"New issue: {issue['title']}\n\n{issue['body']}"
        )},
    ]

    for _ in range(10):  # safety cap on iterations
        response = client.chat.completions.create(
            model="gpt-4o", messages=messages, tools=tools
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return {"analysis": msg.content}

        for tool_call in msg.tool_calls:
            result = execute_tool(
                tool_call.function.name,
                json.loads(tool_call.function.arguments)
            )
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result)
            })

    return {"analysis": "Max iterations reached", "status": "incomplete"}

Given the same "intermittent 500 errors" issue, this agent might:

  1. Search for similar issues and find three related reports from the past week
  2. Check error logs for the payments service and find a spike in timeout errors
  3. Determine this is a P0 duplicate of an existing incident
  4. Post to the on-call channel with a summary linking the related issues and log evidence

The agent adapts to inputs the developer never anticipated. But it costs 3-10x more per run (LLM tokens), takes seconds instead of milliseconds, and its behavior varies between runs. You cannot write a deterministic unit test for it -- you need eval frameworks instead.

The Decision Matrix: When to Use Which

This is the table I wish existed when my team was choosing between architectures:

Dimension Workflow Automation Autonomous Agent
Execution path Fixed at design time Determined at runtime
Predictability High -- same input, same output Lower -- LLM reasoning varies
Cost per run Low -- API calls only Higher -- LLM tokens per step
Latency Fast -- milliseconds per step Slower -- seconds per LLM call
Handles ambiguity Poorly -- needs explicit rules Well -- reasons about edge cases
Debugging Easy -- trace the chain Harder -- inspect reasoning traces
Testing Unit tests Eval frameworks + assertions
Best for Repetitive, well-defined tasks Novel, judgment-heavy tasks

Start with a workflow. If you find yourself writing increasingly complex if/else chains to handle edge cases, that is the signal to introduce agent reasoning -- but only for the steps that need it.

The Hybrid Pattern: Workflows That Trigger Agents

The most effective production architecture is not pure workflow or pure agent. It is a workflow that hands off to an agent only where reasoning is needed.

Consider a daily engineering report. Fetching metrics is deterministic -- the same API call, the same data shape, every time. Analyzing those metrics and writing a coherent summary requires judgment. Posting the result to Slack is deterministic again.

The hybrid pattern uses a workflow for the cheap, predictable parts and an agent for the judgment-heavy part:

from openai import OpenAI

client = OpenAI()

def daily_engineering_report():
    """Hybrid: workflow trigger + agent reasoning + workflow delivery."""

    # Step 1: Deterministic data fetch (workflow -- fast, cheap, predictable)
    metrics = fetch_datadog_metrics(service="api", period="24h")
    issues = fetch_github_issues(repo="acme/backend", state="open")
    deploys = fetch_deploy_log(environment="production", since="24h")

    # Step 2: Agent reasoning (autonomous -- judgment required)
    analysis = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": (
                "You are a senior engineering manager. Analyze today's "
                "metrics, open issues, and deployments. Identify the top "
                "3 priorities and flag any risks. Be specific and concise."
            )},
            {"role": "user", "content": (
                f"Metrics: {metrics}\n\n"
                f"Open issues: {issues}\n\n"
                f"Deploys: {deploys}"
            )}
        ]
    ).choices[0].message.content

    # Step 3: Deterministic delivery (workflow -- predictable, auditable)
    post_to_slack(channel="engineering", message=analysis)
    log_report(content=analysis, timestamp=now())

    return {"status": "delivered", "analysis_length": len(analysis)}

This pattern keeps your costs low (LLM only runs once, not for every step), your delivery reliable (Slack post never depends on LLM reasoning), and your analysis adaptive (the agent can identify risks you did not anticipate in a rule).

Platforms like Nebula are built around this hybrid model -- trigger-based scheduling with autonomous agent execution. Your agent runs on a cron schedule, connects to apps through deterministic integrations, but reasons about what to do at each step rather than following a fixed chain.

The 80/20 Rule for Production Systems

After building production agent systems across multiple teams, the pattern I keep seeing is this: 80% workflow, 20% agent.

Most steps in any process are predictable. Data fetching, formatting, routing, posting -- these do not need an LLM. The 20% that benefits from agent reasoning is the ambiguous part: classifying a ticket that does not match your rules, writing a summary that requires judgment, investigating an alert that could mean three different things.

The mistake teams make is reaching for an agent when a workflow would do. An agent that follows the same path every time is just an expensive workflow. And a workflow drowning in edge-case rules is begging to be replaced by agent reasoning on that specific step.

Stop calling everything an agent. If your system follows a fixed chain of steps with one LLM call for summarization, it is an AI-enhanced workflow. That is not a demotion -- it is the right tool for the job. Reserve agent reasoning for the steps that genuinely require it, and your systems will be cheaper, faster, and easier to debug.

This is part of the Building Production AI Agents series. Previous articles cover cost control patterns, context engineering, and human approval gates for production agents.

I built a free job search tool that scrapes 185k+ engineering jobs by tech stack. Roast it.

2026-03-17 04:02:38

I've been job hunting as a full-stack dev. I usually do freelance work but I'm not the best businessman and honestly it would be nice if somebody just told me what to build and I could get paid. But all the jobs on LinkedIn are ghost jobs, they're just accumulating resumes into a black hole and none of them ever actually get checked. You write a whole cover letter for a job that doesn't exist.

I checked out a bunch of the newer tools. HiringCafe looked like the backend wasn't even hooked up, which is kinda ridiculous. They have money. StackJobs wouldn't load. Scoutify wanted money and I was like nah, I'll just build this myself with Claude. Jobright kinda works but the matching wasn't what I wanted, I wanted to be able to search for specific engineering jobs that match my exact tech stack. Like I build with Django, React, TypeScript, I don't want to read through every JD guessing if they use what I use.

So I built this thing called RepoRadar. I wanted it to be click click results, because my attention span is pretty short.

how it works

Google SSO > upload your resume > Claude API parses it and auto-selects your tech stack > hit search > see matching jobs sorted by recent. This way you avoid the ghost jobs because everything's pulled fresh. You click a job and it takes you to the actual company page to apply. minimal bs.

where the jobs come from

After doing some research it turns out most tech companies use one of like four ATS platforms and they all have public APIs. No auth needed. You can hit them as much as you want which is pretty sweet. So what I did is I mapped over 6,000 companies to their ATS platform Greenhouse, Lever, Ashby, Workable. I created a Celery beat task that hits all of them every morning around 6am and pulls fresh listings.

GET https://boards-api.greenhouse.io/v1/boards/stripe/jobs

That gives you every open role at Stripe in JSON. All four platforms work like this.

I also pull from RemoteOK, Remotive, We Work Remotely, and the monthly HN Who's Hiring thread. But it's mostly from the ATS boards — there's some ridiculous amount that show up on RepoRadar, like 185,000 jobs right now.

The mapping was honestly the hardest part of the whole project. Took way longer than expected, and I kept having to reprocess them and losing my SSH connection to Railway in the middle of it.

how the matching works

Every job description gets run through a regex extractor with ~170 keyword patterns: Django, React, PostgreSQL, TypeScript, Next.js, etc. It tags each job with what it detected. When you search it just filters where your stack overlaps with what's in the JD.

TECH_PATTERNS = {
    'django': r'\bdjango\b',
    'react': r'\breact(?:\.js|js)?\b',
    'postgresql': r'\b(?:postgres(?:ql)?|psql)\b',
    'typescript': r'\btypescript\b',
    'next_js': r'\bnext\.?js\b',
    # ~170 more
}

def extract_techs(description_text):
    detected = []
    text_lower = description_text.lower()
    for tech_name, pattern in TECH_PATTERNS.items():
        if re.search(pattern, text_lower):
            detected.append(tech_name)
    return detected

No ML, no embeddings, no vector search. If I get some traffic maybe I'll add that stuff, who knows. Is it perfect? No. But is it tailored for engineering jobs? Absolutely.

I also wanted to make it so you could search by what companies are actually building with. Like I build with Claude, so I want to find companies that match my exact workflow. That's kind of the vision.

the stack

Django 5 / DRF on Railway, React 19 / TypeScript / Vite / Tailwind on Netlify, PostgreSQL, Redis + Celery for background jobs, Claude API for resume parsing, Google OAuth via django-allauth. Railway runs a single container that starts gunicorn and a Celery worker from a start.sh script. Frontend proxies API requests through netlify.toml. OAuth goes straight to Railway because you can't proxy OAuth redirects — learned that the hard way.

what I learned

ATS public APIs are a goldmine. You can hit them way more than you can hit the GitHub API (which wasn't that useful anyway since most company repos are private). There's so many jobs out there it's actually crazy once you start pulling from these endpoints.

The company-to-ATS mapping took way longer than writing the application. Like, significantly. Ok, maybe an exaggeration. But annoying either way

Freshness matters more than volume. The older a posting is, the less likely you are to hear back from anybody. So everything on RepoRadar is sorted recent-first and refreshed daily.

Originally I had set up a GitHub integration to search repos and find organizations by tech stack then map that way, but it turns out most company repos are private so that didn't work the way I wanted. I pivoted to focusing on the ATS boards and remote platforms instead.

what's not great

6,200 companies is a lot but it's not everything, no Workday, iCIMS, or Taleo coverage yet. Regex misses some edge cases. And if I get concurrent user load I'm gonna have to split Celery out into its own Railway service instead of running it in the same container, but that's all doable.

Right now I've got it hooked up with Sentry for monitoring. I'd love to add OpenTelemetry if I can get some real users on it. I also set up a bunch of MCP servers — Railway, Chrome MCP — but the Chrome one isn't that helpful because you can't get past the Google SSO login screen with it.

try it

It's free. You can try it and complain to me and tell me what I did wrong — in fact I would love that. It'd be really cool if I can get some users on this thing.

Note: I made it so you can filter for us remote only

Email me: [email protected] - Direct complaint line. Also can tell me its cool.

AI Makes Your Firm Faster. Maister Explains Why It Doesn't Make You Richer.

2026-03-17 04:02:08

Every professional services firm is adopting AI right now. Most are seeing speed gains — drafts in minutes, research in hours, code in seconds. Almost none are seeing proportional profit gains. Some are seeing the opposite: delivery gets faster, revenue stays flat, and margin pressure grows.

Why?

Because speed of generation is not leverage. And leverage — where profit actually comes from in a professional firm — was explained with painful clarity by David Maister over 30 years ago. His model didn't predict AI. But it explains precisely what AI breaks, what it doesn't, and where the money actually moves.

If you run a practice, lead a delivery org, or make decisions about how your firm sells and staffs work, this matters more than your AI adoption roadmap.

A Quick Maister Refresher

For those who read Managing the Professional Service Firm a decade ago (and those who keep meaning to), here's the core of it.

Maister observed that all professional work falls into three types:

Brains Gray Hair Procedure
What the client buys Rare expertise Experience & judgment Speed & reliability
Leverage Low Medium High
Price sensitivity Low Medium High
Typical pyramid Flat Medium Wide

Leverage is the ratio of junior staff to senior staff. A firm's profit per partner grows in two ways: either you sell more expensive work (move up the scale) or you deliver the same work with a wider pyramid (more juniors per senior). That's it. That's the entire economics of professional services in one sentence.

And here's the part everyone forgets: Maister noted that work naturally drifts down the scale. What was once brain surgery becomes, over time, more standardized, more procedural. Senior expertise gets codified into checklists, templates, training programs.

AI doesn't change this dynamic. It accelerates it dramatically.

Three Things AI Actually Breaks

A. Not your cost base — the nature of the service itself

AI pushes chunks of work down the Brains → Gray Hair → Procedure scale. What required senior attention yesterday can be partially standardized today and handed to a system instead of a junior.

But — and this is critical — it doesn't push all work down. Real services become hybrid: the discovery and judgment parts stay Gray Hair or Brains, while the synthesis, drafting, comparison, and QA parts shift toward Procedure. Profit emerges at the seam between them.

B. Not your people — the carrier of leverage

In a classic firm, leverage lives in the pyramid: juniors at the bottom producing work, seniors at the top selling judgment. AI compresses the bottom of the pyramid. Extraction, drafting, classification, first-pass review — these are increasingly done by a system, not a team of associates.

This doesn't eliminate leverage. It changes what carries it. Leverage stops being "how many juniors can I put under one senior" and becomes "how much delivery can I move into reliable, repeatable digital workflows while keeping quality and accountability."

C. Not your margin — your billing model

Here's the most uncomfortable shift. AI often raises productivity faster than profitability. If your firm does the same work 3x faster but still sells hours, you've just cut your revenue by two thirds. AI creates value only when a firm knows how to repackage speed into price, scope, throughput, or share of wallet.

The most vulnerable thing in your firm isn't your margin. It's the billable hour.

Work as a Graph

Here's where it gets interesting.

Think about what actually happens inside a project. It's not "a team works for 500 hours." It's a chain of transformations: someone takes an input — client data, a document, a set of requirements — and turns it into an intermediate artifact. Someone else picks up that artifact, transforms it further, and passes it on. Eventually, a final deliverable comes out.

This isn't a metaphor. It's a structure. In technical terms, it's a directed graph: a network of tasks where each task takes an input, produces an output, and passes it to the next node. If you've ever seen a flowchart or a project dependency diagram, you've seen a graph.

In a traditional firm, this graph is executed by people arranged in a pyramid:

  • Partner scopes the problem
  • Manager coordinates the work
  • Juniors produce the artifacts
  • Partner validates the result

Leverage = how many juniors fit under one partner.

In an AI-native firm, the same graph looks different:

  • Senior expert designs the graph
  • System orchestrates execution
  • Model executes a significant portion of intermediate nodes
  • Humans sit at high-stakes checkpoints — verification, escalation, sign-off
  • Firm monetizes the architecture of the graph, not just the effort

Leverage = how many nodes can run reliably without a human in the loop.

This gives us a tighter formula for what a profitable AI-native service line actually looks like:

Brains-framed, Gray-Hair-supervised, Procedure-executed graph.

The expert frames the problem. Experienced judgment governs the critical decisions. AI executes the procedural middle. The firm charges for the architecture, the reliability, and the final sign-off — not for the hours it took.

But here's the catch: this doesn't give you infinite leverage. The graph has its own bottlenecks.

Decomposition. Someone has to break the problem into the right nodes. This is the new elite skill — and it's scarce.

Verification. The more powerful the generation, the more expensive it becomes to check the output. In high-stakes domains, the cost of validation doesn't disappear — it becomes central.

Exceptions. Graphs work beautifully on the standard path. Valuable client work often breaks the pattern. Exception handling snaps you right back into expensive senior judgment.

Context. Each node solves locally, but the client's problem requires global coherence. Stitching local outputs into a coherent answer is its own expensive function.

Trust. Even if 80% of the work was done by a pipeline, someone must sign the final result. In professional services, clients often pay precisely for that signature, that accountability, that trust.

The graph doesn't make leverage infinite. It moves the bottleneck from "production capacity" to "architecture, verification, exceptions, and accountability."

The Trap Everyone Misses

Now here's the question most firms get wrong.

The question every firm asks: Can AI do this work?

The question they should ask: Can we verify that AI did it right — cheaper than doing it ourselves?

This distinction changes everything. Consider:

Easy to verify Hard to verify
Easy to produce Commodity automation False-friend zone
Hard to produce AI-native sweet spot Human-dominant

Commodity automation (easy to produce, easy to verify): formatting, template extraction, rules-based classification. The money is real but commoditizes fast.

AI-native sweet spot (hard to produce, easy to verify): complex software with good test coverage, compliance mapping, structured due diligence, analytics with rubric-based outputs. Generation is expensive and valuable, but verification is cheap — you can build a reliable graph here.

Human-dominant (hard to produce, hard to verify): unique strategic decisions, bespoke negotiations, socially complex transformations. AI helps you think, but doesn't become the engine of delivery.

And then there's the dangerous quadrant.

False-friend zone (easy to produce, hard to verify): the model will happily produce a "convincing" strategy memo, a "solid" analysis, a "professional" report. It looks great in a demo. But verifying that it's actually right — materially correct, substantively sufficient, contextually appropriate — costs almost as much as writing it from scratch.

This is the zone of impressive demos and weak economics. The benefit of cheap generation gets eaten alive by expensive human review.

Here's a helpful litmus test: if your reviewer has to essentially re-think the entire piece to verify it, you're in the false-friend zone. Your AI is creating the illusion of leverage, not the reality.

Software delivery understood this early — not because writing code is easy, but because software has a rich verification layer: tests, types, linters, CI pipelines, staging environments, rollback. You can check correctness without re-doing the work.

Most advisory work hasn't figured this out yet.

Where the Money Actually Is

If you overlay AI's impact onto Maister's three types of work, the picture is clear:

Practice type What AI does Consequence
Procedure Most graphable, most automatable Price drops, transparency rises, margin compresses, consolidation accelerates
Gray Hair Best segment for AI-native capture — client still needs a human, but most delivery lives in the graph, error cost is high, judgment premium holds The sweet spot
Brains Graph augments frontier thinking (more options explored, faster synthesis) but doesn't replace it Stays premium, too narrow for mass money pool

The main prize is Gray Hair work, translated into a managed graph.

Not full automation — that's Procedure, and the margins are heading to zero. Not pure Brains — that's too bespoke to scale. The sweet spot is the middle: work where the client still needs experienced judgment, but where a significant share of delivery can live inside a well-governed, verifiable graph.

What This Means for Your Firm

A few implications worth sitting with:

The transition won't be won by whoever adopts AI first. It will be won by whoever learns to build verifiable graphs — who can decompose expert work into nodes that are cheap to execute, cheap to check, and cheap to re-run.

Practice areas will be defined not just by domain, but by graph economics. The relevant question is no longer "do we have expertise in X?" but "can we build a reliable, verification-friendly delivery graph for X?"

The form of your deliverable becomes a strategic decision. A memo is hard to verify. A memo with a source map, an assumption ledger, and an evidence trail is much easier. Firms that redesign their outputs to be proof-carrying will have structurally better economics.

Automating generation without automating verification is a trap. If you're making AI write drafts but still having seniors review them line by line, you've sped up production while keeping the most expensive bottleneck intact.

We've Built the Operating System for This

This article covers the lens. Behind it, there's a full operational framework.

A structured intake model for evaluating which assignments are actually good candidates for AI-native delivery — and which are false friends. A multi-level assessment protocol that doesn't require building the full graph upfront. A library of assignment archetypes and graph patterns. A set of reshape playbooks that turn "AI-assisted at best" work into graph-friendly delivery. And the automation tooling that brings it all to life.

We use this framework ourselves, and we implement it for firms that are serious about making AI-native delivery actually profitable — not just faster.

If you're running a practice and this resonated, let's talk.

Why Single Agents Fail: Building Scalable AI Teams with the Manager-Worker Pattern

2026-03-17 04:00:00

If you've ever built an AI agent using a simple ReAct loop, you know the pain: it works great for simple tasks, but throw a complex, multi-step problem at it, and the whole system buckles. The agent gets lost in its own context window, forgets earlier constraints, or gets stuck in infinite loops. It’s like hiring a single "full-stack developer" to build an entire enterprise platform from scratch—it’s inefficient and prone to failure.

The solution? Hierarchical Agent Teams. This architectural pattern, inspired by microservices in software engineering, introduces a Manager-Worker structure that scales, modularizes, and stabilizes your AI applications. In this post, we’ll dive deep into the theory, explore the analogy to modern software architecture, and walk through a practical TypeScript implementation using LangGraph.js and Zod.

The Core Concept: The Manager-Worker Pattern

In the previous chapter, we explored the ReAct Loop as a foundational agentic design pattern. This pattern creates a cyclical graph structure where an agent alternates between generating a Thought (internal reasoning), selecting an Action (tool call), and processing an Observation (tool result). While powerful for single-agent tasks, the ReAct loop represents a single, monolithic unit of intelligence. When a problem becomes complex—requiring multiple distinct skill sets, parallel processing, or sequential dependency management—relying on a single agent to handle every step leads to inefficiency, context overload, and a lack of modularity.

Hierarchical Agent Teams solve this by introducing a Manager-Worker pattern. This is a structural architecture where a "Manager" agent (often called a Supervisor or Orchestrator) delegates specific subtasks to specialized "Worker" agents. The Manager does not perform the actual work; instead, it focuses on task decomposition and routing. It analyzes the high-level objective, breaks it down into discrete, manageable units, and assigns each unit to the most competent Worker.

To understand this via a web development analogy, imagine building a complex e-commerce platform. You do not hire a single "Full Stack Developer" to write the entire application from the database schema to the CSS styling in one massive code file. Instead, you assemble a team:

  • The Manager: The Project Manager or Tech Lead. They don't write the code; they read the requirements (the prompt), break them into tickets (subtasks), and assign tickets to the appropriate specialists.
  • The Workers: The Backend Developer (database logic), the Frontend Developer (UI/UX), and the DevOps Engineer (deployment). Each is an expert in their domain.

In LangGraph.js, this hierarchy is not just a conceptual grouping; it is a literal graph structure. The Manager is a node in the graph, and the Workers are sub-graphs or individual nodes that the Manager routes to. This creates a scalable, modular workflow where the failure of one component (e.g., a tool call by a Worker) can be isolated and handled without collapsing the entire system.

Why Hierarchical Architectures are Necessary

The transition from single-agent ReAct loops to multi-agent hierarchies is driven by the limitations of context windows and the complexity of reasoning.

  1. Context Management: A single agent attempting to solve a complex problem (e.g., "Plan a vacation itinerary for 5 days in Tokyo, book flights, and reserve hotels") must hold the entire plan, current status, tool outputs, and future steps in its immediate memory (context window). As the conversation grows, the model risks "forgetting" earlier constraints or hallucinating details. By delegating, the Manager only needs to track the high-level state, while Workers handle the granular details of their specific domain.
  2. Specialization and Consistency: Just as a database engineer writes better SQL than a generalist, a specialized agent can be tuned (via prompt engineering and tool selection) for a specific task. A "Researcher" agent can be optimized for browsing and summarizing, while a "Coder" agent is optimized for writing TypeScript. This prevents the "jack of all trades, master of none" problem.
  3. Parallelism and Efficiency: In a linear ReAct loop, steps are sequential. In a hierarchical system, the Manager can dispatch independent tasks to multiple Workers simultaneously. For example, while one Worker searches for flight prices, another can check hotel availability. The Manager awaits the results of all before synthesizing the final answer.

The Manager: The Orchestrator of Logic

The Manager agent is the brain of the operation. Its primary function is State Management and Routing. In LangGraph.js, the Manager is typically implemented as a node that utilizes an LLM (Large Language Model) to decide the next step based on the current graph state.

The Manager operates on a "plan" or a "mental model" of the available Workers. It doesn't know how a Worker performs a task, only what the Worker is capable of. This is analogous to an API Gateway in microservices architecture. The Gateway (Manager) knows that POST /users creates a user, but it doesn't know the internal implementation details of the User Service (Worker).

The decision-making process of the Manager often involves a Router mechanism. This can be a deterministic function (e.g., if the task involves math, route to the Calculator Worker) or a dynamic LLM call (e.g., "Based on the user's request, which of the following agents is best suited to handle the next step?"). This router ensures that the flow of control follows the most efficient path through the graph.

The Workers: Specialized Execution Units

Workers are the execution engines of the hierarchy. In LangGraph.js, a Worker is defined as a node that possesses a specific set of tools and a specific system prompt. A Worker does not necessarily need to know about the existence of other Workers; its world is limited to its assigned tasks and the tools it can access.

Consider the Tool Use Reflection concept defined earlier. This is crucial for Workers. A Worker might call a tool (e.g., a search API), receive an observation, and then use that observation to refine its internal thought process before making another tool call or handing control back to the Manager. This internal ReAct loop allows the Worker to be autonomous within its domain.

However, unlike a standalone agent, a Worker in a hierarchical team is often "stateless" regarding the overall goal. It processes a specific input (a subtask from the Manager) and produces a specific output (the result). It does not retain memory of the conversation history beyond what is passed in the current state update.

Visualizing the Hierarchy

To visualize this flow, we can look at the graph structure. The Manager acts as a central hub, dispatching tasks to specialized nodes. The Workers process these tasks and return results, which the Manager then synthesizes.

::: {style="text-align: center"}
The Manager node orchestrates the workflow by delegating tasks to specialized Worker nodes, which process the assigned work and return their results for final synthesis.{width=80% caption="The Manager node orchestrates the workflow by delegating tasks to specialized Worker nodes, which process the assigned work and return their results for final synthesis."}
:::

The Web Development Analogy: Microservices vs. Monolith

To deeply understand the "Why" of Hierarchical Agent Teams, let's expand on the web development analogy, specifically comparing Monolithic Architecture vs. Microservices Architecture.

The Monolith (Single ReAct Agent):
In a monolithic web application, the frontend, backend, and database logic are tightly coupled in one codebase. If you need to update the search algorithm, you might have to redeploy the entire application.

  • In Agents: A single ReAct agent handles everything. If the agent gets stuck in a loop trying to format data perfectly while also trying to search for it, the entire "application" freezes. The context window is shared for everything, leading to congestion.

The Microservices (Hierarchical Agents):
In a microservices architecture, the application is broken down into small, independent services that communicate over a network (like HTTP or gRPC). Each service owns its own data and logic.

  • In Agents: The Manager is the API Gateway. It receives a request (User Prompt). It routes the request to the Search Service (Researcher Worker). The Search Service queries its own database (Vector Store) and returns a JSON response. The Manager then routes that data to the Processing Service (Analyst Worker) to summarize it. Finally, the Manager formats the response for the user.

The "Under the Hood" Connection:
Just as microservices use standardized protocols (REST/GraphQL) to ensure different services can talk to each other, Hierarchical Agents use a standardized State Schema. In LangGraph.js, the State object is the shared language between the Manager and the Workers. If a Worker expects a query string in the state and the Manager provides a query object, the system breaks—just as if a microservice expected JSON but received XML.

Task Decomposition and State Propagation

The magic of the Manager-Worker pattern lies in how information flows. It is not a simple linear chain; it is a recursive or iterative flow.

  1. Decomposition: The Manager looks at the initial state (User Request). It uses an LLM to generate a list of subtasks. For example, "Write a report on AI trends" becomes:
    • Subtask 1: Search for recent AI news.
    • Subtask 2: Summarize the key findings.
    • Subtask 3: Draft the report.
  2. Dispatch: The Manager updates the graph state with the first subtask and routes control to the Researcher Worker.
  3. Execution: The Researcher Worker executes its internal ReAct loop (Thought -> Action -> Observation) using its specific tools (e.g., search_web).
  4. Aggregation: The Researcher Worker writes its findings back to the shared state (e.g., state.research_data = [...]). Control returns to the Manager.
  5. Iteration: The Manager reviews the updated state. It sees that research_data is populated but summary is empty. It routes control to the Analyst Worker.

This propagation ensures that the "knowledge" gained by one specialized agent is available to the next, without the agents needing to communicate directly with one another. They communicate indirectly through the shared state managed by the Manager.

Scalability and Error Handling

Hierarchical systems offer superior scalability. If a specific task requires heavy computation (like analyzing a large PDF), we can scale that specific Worker node independently. In LangGraph.js, this can be implemented by offloading a Worker node to a separate server or queue system, while the Manager remains lightweight.

Furthermore, error handling becomes granular. If the Researcher Worker fails to find a result (e.g., the search tool returns an error), the Manager can detect this state change (e.g., state.error = true) and route to a "Fallback Worker" or attempt the task with a different tool. In a monolithic agent, an error in a tool call often requires restarting the entire reasoning process from scratch.

Summary of Theoretical Foundations

The Hierarchical Agent Team is not merely a way to organize prompts; it is a structural paradigm for managing complexity. By separating the concerns of Orchestration (Manager) from Execution (Workers), we create systems that are:

  • Modular: Components can be swapped or updated without affecting the whole.
  • Scalable: Heavy tasks can be isolated and parallelized.
  • Robust: Errors are contained within specific nodes.
  • Context-Efficient: Memory is distributed across the graph, reducing the load on the LLM's context window.

This architecture mirrors the evolution of software engineering from monoliths to distributed systems, applying the same proven principles of separation of concerns to the domain of artificial intelligence.

Practical Implementation: A Customer Support SaaS

In a hierarchical multi-agent system, a Supervisor Node acts as a traffic controller. It doesn't perform the specialized work itself; instead, it analyzes the current state of the application (e.g., a user request in a SaaS dashboard) and decides which Worker Agent is best suited to handle the next step. The Supervisor uses structured output (often JSON) to delegate tasks clearly, ensuring the receiving worker knows exactly what to do.

This example simulates a simple Customer Support SaaS Application. We have a Supervisor that routes user queries to either a BillingWorker (for payment issues) or a TechnicalWorker (for bugs). We will use zod for schema validation to ensure the Supervisor's output is strictly typed and reliable.

Visualizing the Flow

The following diagram illustrates the control flow. The Supervisor receives the initial request, makes a decision, and invokes the appropriate worker node. The worker then updates the state, and the process terminates.

::: {style="text-align: center"}
The Supervisor receives the initial request, makes a decision, and invokes the appropriate worker node, which then updates the state and terminates the process.{width=80% caption="The Supervisor receives the initial request, makes a decision, and invokes the appropriate worker node, which then updates the state and terminates the process."}
:::

Implementation

This code is fully self-contained. It simulates the LLM calls using mock functions to ensure it runs without external API keys, but the structure mirrors a real production environment using LangGraph.js and Zod.

import { StateGraph, Annotation, StateSendMessage, StateSend } from "@langchain/langgraph";
import { z } from "zod";

// ==========================================
// 1. Define State and Schemas
// ==========================================

/**
 * The shared Graph State. This object is passed between nodes.
 * In a real app, this would contain user session data, conversation history, etc.
 */
type GraphState = {
  userRequest: string;
  route: string | null; // The decision made by the Supervisor
  finalResponse: string | null; // The result from the worker
};

// Schema for the Supervisor's decision.
// The Supervisor MUST output JSON matching this schema.
const SupervisorDecisionSchema = z.object({
  route: z.enum(["billing", "technical"]).describe("The worker to delegate the task to."),
  reasoning: z.string().describe("Why this route was chosen."),
});

// ==========================================
// 2. Define Agent Nodes
// ==========================================

/**
 * Supervisor Node: Analyzes the state and decides which worker to invoke.
 * In a real scenario, this would call an LLM (e.g., GPT-4) with a structured output prompt.
 * Here, we mock the logic for clarity and reliability.
 */
async function supervisorNode(state: GraphState): Promise<Partial<GraphState>> {
  console.log(`[Supervisor] Analyzing: "${state.userRequest}"`);

  // Mock LLM decision logic based on keywords
  let decision: z.infer<typeof SupervisorDecisionSchema>;

  if (state.userRequest.toLowerCase().includes("bill") || state.userRequest.toLowerCase().includes("charge")) {
    decision = { route: "billing", reasoning: "User mentioned billing terms." };
  } else if (state.userRequest.toLowerCase().includes("bug") || state.userRequest.toLowerCase().includes("error")) {
    decision = { route: "technical", reasoning: "User mentioned technical issues." };
  } else {
    // Default fallback
    decision = { route: "technical", reasoning: "Unclear request, defaulting to technical support." };
  }

  // Validate the output against the schema (Defensive Programming)
  const validatedDecision = SupervisorDecisionSchema.parse(decision);

  console.log(`[Supervisor] Decision: Route to ${validatedDecision.route}`);

  // Update state with the decision
  return { route: validatedDecision.route };
}

/**
 * Worker Node: Billing Specialist.
 * Handles specific logic related to invoices and payments.
 */
async function billingWorker(state: GraphState): Promise<Partial<GraphState>> {
  console.log(`[Billing Worker] Processing request...`);
  // Simulate database lookup or API call
  const response = `Billing Report: Your last invoice #12345 was paid successfully. No issues found regarding "${state.userRequest}".`;
  return { finalResponse: response };
}

/**
 * Worker Node: Technical Support.
 * Handles bugs, errors, and system functionality.
 */
async function technicalWorker(state: GraphState): Promise<Partial<GraphState>> {
  console.log(`[Technical Worker] Processing request...`);
  // Simulate debugging logic
  const response = `Technical Analysis: We investigated the error regarding "${state.userRequest}". A patch has been deployed.`;
  return { finalResponse: response };
}

// ==========================================
// 3. Define Routing Logic (Edges)
// ==========================================

/**
 * Conditional Edge: Determines the next step based on the Supervisor's decision.
 * This is the "Delegation Strategy".
 */
function routeDecision(state: GraphState): string | typeof StateSendMessage {
  if (!state.route) {
    // If the supervisor hasn't decided yet, stay on the supervisor (loop prevention)
    return "supervisor"; 
  }

  // Route to the specific worker node based on the 'route' field in state
  if (state.route === "billing") {
    return "billing_worker";
  } else if (state.route === "technical") {
    return "technical_worker";
  }

  // If we reach here, the state is invalid or unknown
  return StateSendMessage("Invalid routing decision detected.");
}

// ==========================================
// 4. Construct the Graph
// ==========================================

/**
 * Initialize the State Graph.
 * We use a mutable state object where nodes update specific fields.
 */
const workflow = new StateGraph<GraphState>({
  // Define the schema of the state (optional in JS, but good for TS inference)
  stateSchema: {
    userRequest: { value: null, reducer: (prev, next) => next ?? prev },
    route: { value: null, reducer: (prev, next) => next ?? prev },
    finalResponse: { value: null, reducer: (prev, next) => next ?? prev },
  },
});

// Add nodes to the graph
workflow.addNode("supervisor", supervisorNode);
workflow.addNode("billing_worker", billingWorker);
workflow.addNode("technical_worker", technicalWorker);

// Define the entry point
workflow.setEntryPoint("supervisor");

// Define conditional edges from the supervisor
// "supervisor" node -> checks routeDecision -> goes to "billing_worker" or "technical_worker"
workflow.addConditionalEdges("supervisor", routeDecision);

// Define terminal edges (workers go to END)
workflow.addEdge("billing_worker", StateSendMessage("END"));
workflow.addEdge("technical_worker", StateSendMessage("END"));

// Compile the graph
const app = workflow.compile();

// ==========================================
// 5. Execution
// ==========================================

/**
 * Helper to run the graph and log results.
 */
async function runTest(request: string) {
  console.log("\n----------------------------------------");
  console.log(`Starting Execution: "${request}"`);
  console.log("----------------------------------------");

  const initialState: GraphState = {
    userRequest: request,
    route: null,
    finalResponse: null,
  };

  // Stream events to see the flow in real-time
  const stream = await app.stream(initialState);

  for await (const event of stream) {
    // The stream yields updates from nodes
    const nodeName = Object.keys(event)[0];
    const nodeState = event[nodeName];

    if (nodeState.finalResponse) {
      console.log(`\n>>> FINAL OUTPUT: ${nodeState.finalResponse}`);
    }
  }
}

// Run simulations
(async () => {
  // Test Case 1: Billing Issue
  await runTest("I think there is a problem with my invoice charge.");

  // Test Case 2: Technical Issue
  await runTest("The dashboard is throwing a 500 error.");
})();

Detailed Line-by-Line Explanation

Here is the breakdown of the logic into a numbered list for clarity.

1. State and Schema Definition

  • GraphState Type: Defines the shape of the data flowing through the graph. It tracks the user's request, the routing decision (route), and the final output. In a real SaaS app, this might also include userId, sessionId, or authToken.
  • SupervisorDecisionSchema (Zod): This is critical for reliability. Instead of asking the LLM to output free text, we enforce a JSON structure. Zod ensures that the Supervisor's output is validated at runtime. If the LLM hallucinates a format, Zod throws an error, preventing downstream crashes.

2. Agent Nodes

  • supervisorNode: This is the "Brain" of the hierarchy.
    • It receives the current state.
    • Under the Hood: In a production environment, you would pass state.userRequest to an LLM prompt like: "Analyze the user request and output JSON: { route: 'billing' | 'technical' }".
    • Mock Logic: For this "Hello World" example, we use simple string matching (includes) to simulate the LLM's decision-making process.
    • Validation: SupervisorDecisionSchema.parse(decision) validates the decision. This is a best practice to handle LLM non-determinism.
  • billingWorker & technicalWorker: These are the "Hands". They only care about their specific domain. They receive the state (which now includes the route decision) and perform the actual business logic (e.g., querying a database, calling an API). They return an updated state containing the finalResponse.

3. Routing Logic (Edges)

  • routeDecision Function: This function acts as the router. It looks at the state.route field populated by the Supervisor.
    • It returns a string matching the name of the next node to execute (e.g., "billing_worker").
    • This separation of logic (Node) and flow control (Edge) is a core strength of LangGraph. It allows you to visualize the graph flow independently of the node code.

4. Graph Construction

  • new StateGraph: Initializes the graph builder. We pass the TypeScript type GraphState for full type safety.
  • workflow.addNode: Registers the functions we defined as nodes in the graph.
  • workflow.setEntryPoint: Defines where the graph starts (always the Supervisor in this pattern).
  • workflow.addConditionalEdges: This is the dynamic part. Instead of a fixed path (A -> B -> C), the graph asks routeDecision where to go after the Supervisor runs.
  • workflow.compile: Turns the declarative definition into an executable runtime object.

5. Execution

  • app.stream: This method executes the graph. It returns an async iterator, allowing us to "watch" the graph execute step-by-step. This is useful for real-time UI updates in a web app (e.g., showing a loading spinner while the Supervisor decides, then showing the worker's result).

Common Pitfalls

When building hierarchical agents in TypeScript/LangGraph, watch out for these specific issues:

  1. LLM Hallucination & JSON Parsing Errors

    • The Issue: LLMs often output invalid JSON or add conversational text (e.g., "Sure, here is the JSON: { ... }") which breaks JSON.parse().
    • The Fix: Never rely on JSON.parse directly on raw LLM output. Use a schema validator like Zod (as shown in the code) or LangChain's withStructuredOutput. This forces the LLM to adhere to a strict schema and handles parsing errors gracefully.
  2. State Mutation & Reference Issues

    • The Issue: In JavaScript/TypeScript, objects are passed by reference. If you mutate the state object directly inside a node (e.g., state.route = 'billing'), you might cause side effects or race conditions in concurrent streams.
    • The Fix: Always return a new partial state object (e.g., { route: 'billing' }). LangGraph handles merging this partial object into the main state immutably. Avoid mutating the state argument directly.
  3. Infinite Loops

    • The Issue: If the Supervisor fails to make a decision or the routing logic fails, the graph might loop back to the Supervisor indefinitely, consuming expensive LLM tokens.
    • The Fix: Implement a max_iterations counter in your graph state or use a "fallback" node. In the routeDecision function, ensure there is a default path or an error state that terminates the graph.
  4. Vercel/AWS Lambda Timeouts

    • The Issue: Serverless functions have strict timeouts (e.g., 10 seconds on Vercel Hobby plans). Hierarchical graphs involve multiple LLM calls and processing steps, which can easily exceed this limit.
    • The Fix:
      • Streaming: Use app.stream() instead of app.invoke() to send incremental updates to the client, keeping the connection alive.
      • Background Execution: For long workflows, trigger the graph execution via a background job (e.g., Vercel Background Functions, Inngest, or AWS SQS) and notify the frontend via WebSockets or polling when the result is ready.
  5. Async/Await Loops in Streams

    • The Issue: When iterating over app.stream(), failing to await properly or mixing synchronous logic with async streams can block the event loop, causing performance degradation in Node.js.
    • The Fix: Always use for await (const ev to iterate over the stream asynchronously, ensuring non-blocking execution.

Conclusion

The transition from monolithic single-agent systems to hierarchical multi-agent teams is a necessary evolution for building complex, production-ready AI applications. By adopting the Manager-Worker pattern, you gain modularity, scalability, and robustness. The Manager handles the high-level orchestration and state management, while specialized Workers execute domain-specific tasks with precision.

This architecture not only mirrors proven software engineering principles like microservices but also addresses the unique challenges of AI, such as context window limitations and reasoning complexity. By implementing this pattern with LangGraph.js and Zod, you can create systems that are both powerful and maintainable, ready to handle the demands of real-world SaaS applications.

The concepts and code demonstrated here are drawn directly from the comprehensive roadmap laid out in the book Autonomous Agents. Building Multi-Agent Systems and Workflows with LangGraph.js Amazon Link of the AI with JavaScript & TypeScript Series.
The ebook is also on Leanpub.com: https://leanpub.com/JSTypescriptAutonomousAgents.