Skip to content
Learn Agentic AI12 min read0 views

Building a General Contractor Agent: Subcontractor Coordination and Project Management

Learn how to build an AI agent that coordinates subcontractors across trades, manages construction schedules, tracks budgets against estimates, and handles change orders for general contractors.

The General Contractor's Coordination Challenge

A general contractor on a commercial build-out might coordinate 15-20 different subcontractors: demolition, framing, electrical, plumbing, HVAC, drywall, painting, flooring, fire protection, and more. Each trade depends on others finishing first, and every schedule change cascades through the entire project. An AI agent that manages this coordination — tracking who needs to be where, when, and ensuring the right trade is scheduled after its prerequisites are complete — transforms the GC's ability to run multiple projects simultaneously.

The core problem is information flow. When the plumber finishes rough-in a day early, the drywall crew could start sooner — but only if someone tells them. The agent is that someone.

Trade Dependency Management

Construction follows a strict sequence. The agent models trade dependencies and determines which subcontractors can be scheduled at any given point.

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional

@dataclass
class TradePhase:
    trade: str
    phase: str                    # "rough_in", "finish", "trim"
    dependencies: list[str]       # list of "trade:phase" that must complete first
    estimated_days: int
    subcontractor_id: Optional[str] = None
    scheduled_start: Optional[datetime] = None
    actual_start: Optional[datetime] = None
    actual_end: Optional[datetime] = None
    status: str = "pending"       # pending, scheduled, in_progress, complete, blocked

class TradeCoordinator:
    def __init__(self, phases: list[TradePhase]):
        self.phases = {f"{p.trade}:{p.phase}": p for p in phases}

    def get_ready_trades(self) -> list[TradePhase]:
        ready = []
        for key, phase in self.phases.items():
            if phase.status != "pending":
                continue
            deps_met = all(
                self.phases.get(dep, TradePhase("", "", [], 0)).status == "complete"
                for dep in phase.dependencies
            )
            if deps_met:
                ready.append(phase)
        return ready

    def complete_phase(self, trade: str, phase: str) -> dict:
        key = f"{trade}:{phase}"
        current = self.phases.get(key)
        if not current:
            return {"error": f"Phase {key} not found"}
        current.status = "complete"
        current.actual_end = datetime.now()

        newly_ready = self.get_ready_trades()
        return {
            "completed": key,
            "newly_available": [f"{p.trade}:{p.phase}" for p in newly_ready],
            "notification_targets": [
                {
                    "subcontractor_id": p.subcontractor_id,
                    "trade": p.trade,
                    "phase": p.phase,
                    "message": f"{trade} {phase} is complete. Your work can begin.",
                }
                for p in newly_ready
                if p.subcontractor_id
            ],
        }

# Example: typical commercial build-out sequence
COMMERCIAL_BUILDOUT = [
    TradePhase("demolition", "full", [], 3),
    TradePhase("framing", "rough", ["demolition:full"], 5),
    TradePhase("electrical", "rough_in", ["framing:rough"], 4),
    TradePhase("plumbing", "rough_in", ["framing:rough"], 4),
    TradePhase("hvac", "rough_in", ["framing:rough"], 3),
    TradePhase("inspection", "rough", ["electrical:rough_in", "plumbing:rough_in", "hvac:rough_in"], 1),
    TradePhase("insulation", "install", ["inspection:rough"], 2),
    TradePhase("drywall", "hang", ["insulation:install"], 3),
    TradePhase("drywall", "finish", ["drywall:hang"], 4),
    TradePhase("painting", "prime_paint", ["drywall:finish"], 3),
    TradePhase("flooring", "install", ["painting:prime_paint"], 3),
    TradePhase("electrical", "trim", ["painting:prime_paint"], 2),
    TradePhase("plumbing", "trim", ["painting:prime_paint"], 2),
    TradePhase("hvac", "trim", ["painting:prime_paint"], 1),
]

Budget Tracking Against Estimates

The agent tracks actual costs against the original estimate and flags budget variances in real time.

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

@dataclass
class BudgetLineItem:
    category: str
    estimated_amount: float
    committed_amount: float = 0.0    # Subcontract value
    spent_amount: float = 0.0        # Invoices paid
    pending_invoices: float = 0.0

    @property
    def variance(self) -> float:
        return self.estimated_amount - (self.spent_amount + self.pending_invoices)

    @property
    def variance_percentage(self) -> float:
        if self.estimated_amount == 0:
            return 0
        return (self.variance / self.estimated_amount) * 100

class BudgetTracker:
    def __init__(self, line_items: list[BudgetLineItem]):
        self.items = {item.category: item for item in line_items}

    def record_expense(self, category: str, amount: float, invoice_id: str) -> dict:
        item = self.items.get(category)
        if not item:
            return {"error": f"Category {category} not in budget"}
        item.spent_amount += amount
        alert = None
        if item.variance_percentage < -5:
            alert = {
                "type": "over_budget",
                "category": category,
                "overage": abs(item.variance),
                "message": f"{category} is {abs(item.variance_percentage):.1f}% over budget",
            }
        return {
            "category": category,
            "invoice_id": invoice_id,
            "amount": amount,
            "remaining_budget": round(item.variance, 2),
            "variance_pct": round(item.variance_percentage, 1),
            "alert": alert,
        }

    def get_budget_summary(self) -> dict:
        total_estimated = sum(i.estimated_amount for i in self.items.values())
        total_spent = sum(i.spent_amount for i in self.items.values())
        total_pending = sum(i.pending_invoices for i in self.items.values())
        over_budget = [
            {"category": cat, "overage": round(abs(item.variance), 2)}
            for cat, item in self.items.items()
            if item.variance < 0
        ]
        return {
            "total_estimated": round(total_estimated, 2),
            "total_spent": round(total_spent, 2),
            "total_pending": round(total_pending, 2),
            "total_remaining": round(total_estimated - total_spent - total_pending, 2),
            "overall_variance_pct": round(
                ((total_estimated - total_spent - total_pending) / total_estimated) * 100, 1
            ),
            "categories_over_budget": over_budget,
        }

Change Order Management

Change orders are inevitable. The agent captures scope changes, calculates cost impact, and manages the approval workflow.

from enum import Enum

class ChangeOrderStatus(Enum):
    DRAFT = "draft"
    SUBMITTED = "submitted"
    APPROVED = "approved"
    REJECTED = "rejected"

@dataclass
class ChangeOrder:
    co_number: int
    description: str
    reason: str
    requested_by: str
    cost_impact: float
    schedule_impact_days: int
    status: ChangeOrderStatus = ChangeOrderStatus.DRAFT
    trades_affected: list[str] = field(default_factory=list)

class ChangeOrderManager:
    def __init__(self, db, budget_tracker: BudgetTracker):
        self.db = db
        self.budget = budget_tracker
        self.next_co_number = 1

    async def create_change_order(
        self, description: str, reason: str, requested_by: str,
        cost_items: list[dict], schedule_impact_days: int,
        trades_affected: list[str],
    ) -> ChangeOrder:
        total_cost = sum(item["amount"] for item in cost_items)
        co = ChangeOrder(
            co_number=self.next_co_number,
            description=description,
            reason=reason,
            requested_by=requested_by,
            cost_impact=total_cost,
            schedule_impact_days=schedule_impact_days,
            trades_affected=trades_affected,
        )
        self.next_co_number += 1

        await self.db.execute(
            """INSERT INTO change_orders
               (co_number, description, reason, requested_by,
                cost_impact, schedule_impact_days, status)
               VALUES ($1, $2, $3, $4, $5, $6, $7)""",
            co.co_number, description, reason, requested_by,
            total_cost, schedule_impact_days, co.status.value,
        )
        return co

    async def approve_change_order(self, co_number: int) -> dict:
        co = await self.db.fetchrow(
            "SELECT * FROM change_orders WHERE co_number = $1", co_number
        )
        if not co:
            return {"error": f"Change order #{co_number} not found"}

        await self.db.execute(
            "UPDATE change_orders SET status = 'approved' WHERE co_number = $1",
            co_number,
        )
        budget_update = self.budget.record_expense(
            "change_orders", co["cost_impact"], f"CO-{co_number}"
        )
        return {
            "co_number": co_number,
            "status": "approved",
            "cost_impact": co["cost_impact"],
            "schedule_impact_days": co["schedule_impact_days"],
            "budget_update": budget_update,
        }

FAQ

How does the agent handle trades that can work in parallel?

The dependency graph identifies which trades are independent of each other. After framing rough-in is complete, electrical, plumbing, and HVAC rough-in can all proceed simultaneously. The agent recognizes this and sends scheduling notifications to all three subcontractors at once, along with space-sharing coordination to prevent conflicts (e.g., plumber gets kitchen first while electrician starts in bedrooms).

What happens when a subcontractor no-shows?

The agent detects the no-show when the expected check-in does not occur by the scheduled start time. It immediately alerts the GC, calculates the schedule impact, and queries the approved subcontractor list for available replacements with the same trade license. It provides the GC with options ranked by availability and past reliability rating.

How does the change order process prevent scope creep?

Every change order goes through a formal workflow: draft, submit with cost and schedule impact, approve or reject. The agent enforces this by requiring cost justification and trade impact analysis before submission. It also maintains a running total of all approved change orders against the original contract value, giving the GC and owner clear visibility into how changes are affecting the total project cost.


#GeneralContractor #SubcontractorCoordination #ProjectManagement #BudgetTracking #ChangeOrders #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.