DEV Community

Abdul Rehman
Abdul Rehman

Posted on

How to Build a Production AI Agent That Won't Delete Your Database

Suppose an agent is asked to "clean up duplicate entries." It could interpret that as "delete everything where the title field matches more than one record," which might match records with a common word in the title. A cascade delete in under four seconds, and a database with no point-in-time recovery. That's a bad day.

I've seen close calls like that. Not because the agent was malicious. Because the prompt and the agent's threshold for "stale" differed from the developer's intent. Since then I've built agents for job platforms, real estate matching, legal document analysis, and document intelligence systems. Every project reinforced the same patterns. Here's what I've learned about keeping AI agents from destroying your business.

The Three Failure Modes That Will Bite You

Production agents fail in predictable ways. The first is tool misuse. Give an agent a delete function and it will eventually call it on the wrong data. Not because it's malicious. Because the prompt said "remove stale entries" and the agent's threshold for "stale" differs from yours.

The second is hallucination cascades. An agent generates a plausible intermediate result, then acts on that result as if it were verified fact. One wrong assumption compounds into a chain of bad decisions.

The third is permission creep. You start with a read-only agent. Then someone adds a write capability for a specific use case. Then another. Six months later your agent has database admin credentials and nobody remembers why.

Every production incident I've dealt with or observed traces back to one of these three.

Human in the Loop Is Not Optional

I scoped an autonomous apply system for a job platform. The agent would find matching jobs, fill out applications, and submit them. The client wanted it fully automated. Instead, every application goes through a swipe interface. The agent prepares the application, screenshots the filled forms, and presents them to the user for approval. The user swipes right to send, left to discard. The agent never touches the submit button without a human signal.

This approach cost development time. It also prevents scenarios like the agent applying to dozens of jobs with the wrong resume template, which can happen when a prompt misreads "senior engineer" as "entry level."

The pattern is simple: destructive actions require human confirmation. Define destructive broadly. Sending emails. Deleting data. Making payments. Updating production records. If the action has irreversible consequences, a human needs to approve it.

// The pattern I use for human-in-the-loop approval
interface AgentAction {
  id: string;
  type: 'delete' | 'update' | 'create' | 'send';
  target: string;
  payload: unknown;
  risk: 'low' | 'medium' | 'high';
  requiresApproval: boolean;
  approvedBy?: string;
  approvedAt?: Date;
}

class ActionQueue {
  private pending: AgentAction[] = [];

  async propose(action: Omit<AgentAction, 'id'>): Promise<string> {
    const id = crypto.randomUUID();
    this.pending.push({ ...action, id, requiresApproval: action.risk !== 'low' });
    if (action.risk === 'high') {
      await this.notifyHuman(id, action);
    }
    return id;
  }

  async approve(id: string, userId: string): Promise<void> {
    const action = this.pending.find(a => a.id === id);
    if (!action) throw new Error('Action not found');
    action.approvedBy = userId;
    action.approvedAt = new Date();
    await this.execute(action);
  }
}
Enter fullscreen mode Exit fullscreen mode

The queue buys you time. It forces the agent to articulate what it wants to do before doing it. That alone catches most bad decisions.

Sandbox Everything, Trust Nothing

Every agent I build runs in a sandboxed environment. The database connection uses a read-replica for any exploratory queries. Write operations go through a restricted API layer that validates every mutation against a schema.

For browser automation agents, I use headless Chrome in a disposable container. The agent gets a fresh browser profile every session. No cookies, no localStorage, no saved credentials. It starts clean and ends clean.

The job platform's autonomous apply module uses this exact pattern. The agent logs into a test account, navigates the application flow, and screenshots each step. The user reviews the screenshots before the agent can access the real production credentials. Even then, the agent never stores credentials. They're injected at runtime and revoked after the session.

// Sandboxed database access pattern
class AgentDatabase {
  private connection: Pool;

  constructor(readOnly: boolean = true) {
    this.connection = new Pool({
      // Read replica for exploration
      connectionString: readOnly 
        ? process.env.READ_REPLICA_URL 
        : process.env.WRITE_ENDPOINT_URL,
      max: 5,
    });
  }

  async query(sql: string, params: unknown[]): Promise<unknown[]> {
    // Block dangerous operations at the application layer
    const normalized = sql.trim().toLowerCase();
    if (normalized.startsWith('drop') || normalized.startsWith('truncate')) {
      throw new Error('Destructive operations require human approval');
    }
    // Rate limit aggressive queries
    if (this.isHeavyQuery(normalized)) {
      await this.queueForReview(sql);
      return [];
    }
    return this.connection.query(sql, params);
  }
}
Enter fullscreen mode Exit fullscreen mode

The read-replica pattern alone is worth it. An agent can accidentally run an unindexed query on a large collection. The read replica handles it. The production primary never feels a thing.

Idempotency Is Your Safety Net

Idempotent operations are the single most underrated pattern in agent design. If an agent can retry an operation safely, you've eliminated an entire class of failure.

Every write operation in my agent systems uses idempotency keys. The agent generates a unique key before making any mutation. If the operation succeeds but the agent times out waiting for confirmation, it retries with the same key. The system sees the duplicate key and returns the original result without re-executing.

This matters more than you think. Agents retry. They retry aggressively. Without idempotency, a single timeout becomes a double charge, a duplicate entry, or a corrupted state.

// Idempotent mutation wrapper
async function executeWithIdempotency<T>(
  operation: () => Promise<T>,
  key: string
): Promise<T> {
  const existing = await cache.get(key);
  if (existing) return existing as T;

  const result = await operation();
  await cache.set(key, result, { ttl: 3600 }); // 1 hour window
  return result;
}

// Agent side
const idempotencyKey = `${sessionId}:${Date.now()}:${crypto.randomUUID()}`;
await executeWithIdempotency(
  () => api.createApplication(formData),
  idempotencyKey
);
Enter fullscreen mode Exit fullscreen mode

The cache doesn't need to be permanent. An hour window covers the vast majority of retry scenarios. After that, if the agent retries, it's a genuinely new attempt and should execute fresh.

Rollback Strategies That Actually Work

Every agent operation should be reversible. Not theoretically. Practically.

For database mutations, I wrap agent actions in database transactions. If the agent's next step fails, the entire chain rolls back. For external API calls (sending emails, creating tickets), I maintain a compensation log. Each action gets a compensating action that undoes it.

The compensation log is a simple table: action_id, action_type, payload, compensating_action, status. When something goes wrong, I replay the compensating actions in reverse order. It's not perfect for every scenario, but it covers the common cases.

For the legal document analysis system I built, every document parse and clause extraction is logged with a version hash. If the agent generates a flawed risk assessment, the system can roll back to the previous version and flag the document for manual review. The agent never modifies the original document, it works on a copy until the human signs off.

The One Rule I Never Break

Don't give agents access to anything they haven't explicitly asked for.

No blanket database permissions. No "you can access any table" instructions. No API keys with wildcard scopes. Every capability the agent has must be justified by a specific, documented use case.

This sounds obvious. I've seen teams violate it under pressure to ship faster. The result is the same every time: a cascade delete, a mass email sent to the wrong list, a production data corruption. The fix is always the same too. Restrict permissions to the minimum needed for each specific task. Approve every escalation. Log every action.

If your team is building agents that touch production data and you're shipping slower because of safety concerns, that's the kind of thing I help with, happy to compare notes on what guardrails actually scale.


Written by Abdul Rehman, full-stack AI engineer building production SaaS, MVPs, and AI automation. More at PrimeStrides.

Top comments (0)