How I Built a Personal Assistant AI Agent with n8n in 2 Hours (No Code Step-by-Step)

I spent three weeks manually juggling calendar invites, email responses, and task reminders before realizing I was basically a very expensive administrative assistant to myself. That’s when I decided to build my own AI personal assistant using n8n, and honestly, it changed everything.

Table of Contents

  • Why I Chose n8n for My AI Assistant
  • Setting Up Your n8n Environment
  • Building the Core AI Brain
  • Creating Smart Email Management
  • Adding Calendar Intelligence
  • Task Management Integration
  • Testing and Fine-Tuning
  • What I Learned (And Mistakes to Avoid)

Process Overview

Table of Content

Why I Chose n8n

Setting Up Your

Building the Cor

Creating Smart E

Why I Chose n8n for My AI Assistant {#why-i-chose-n8n}

After testing Zapier, Make.com, and a handful of other automation tools, n8n felt like the sweet spot between power and simplicity. Think of it as the Swiss Army knife of workflow automation, but one that doesn’t require a computer science degree to operate.

The biggest selling point? It’s free for personal use and runs locally on your machine. No monthly subscriptions eating into your budget, and your data stays private. Plus, the visual workflow builder makes complex logic feel like connecting LEGO blocks.

Here’s what made n8n perfect for this project:

  • Native AI model integrations (OpenAI, Claude, etc.)
  • 400+ pre-built nodes for popular services
  • Self-hosted option means complete data control
  • JavaScript support when you need custom logic
  • Active community with tons of workflow templates

The learning curve exists, but it’s more like a gentle hill than Mount Everest. You’ll be building useful automations within your first hour.

Setting Up Your n8n Environment {#setting-up-n8n}

First things first, you need n8n running on your system. I’ll walk you through the easiest setup method that doesn’t require any technical wizardry.

Option 1: Desktop App (Recommended for Beginners)

  1. Head to n8n.io and download the desktop application
  2. Install it like any other software (next, next, finish)
  3. Launch the app and you’ll see the workflow canvas
  4. Create a free n8n.cloud account to sync your workflows

Option 2: Docker (If You’re Feeling Adventurous)

docker run -it --rm --name n8n -p 5678:5678 n8nio/n8n

Then navigate to localhost:5678 in your browser.

Essential Integrations to Set Up:

Before building anything, connect these services in your n8n settings:

  • OpenAI API (for the AI brain)
  • Gmail or Outlook (email management)
  • Google Calendar (scheduling intelligence)
  • Notion or Todoist (task management)
  • Slack or Discord (notifications)

I recommend starting with OpenAI’s GPT-4 API. Yes, it costs money (about $0.002 per 1K tokens), but the intelligence difference is worth every penny. You’ll spend maybe $5-10 per month unless you’re having philosophical debates with your assistant.

Building the Core AI Brain {#building-the-ai-brain}

This is where the magic happens. We’re creating the central intelligence that will understand your requests and decide what actions to take.

Step 1: Create the Main Workflow

  1. Start a new workflow in n8n
  2. Add a “Webhook” node as your trigger
  3. Set it to accept POST requests
  4. Copy the webhook URL (you’ll need this later)

Step 2: Add the AI Decision Maker

  1. Connect an “OpenAI” node to your webhook
  2. Choose “Chat Completion” as the operation
  3. Set the model to “gpt-4” (trust me on this one)
  4. In the system message, paste this prompt:
You are a personal AI assistant. Analyze the user's request and categorize it into one of these actions:
- EMAIL: User wants to send, check, or manage emails
- CALENDAR: User wants to schedule, check, or manage calendar events
- TASK: User wants to create, update, or check tasks
- GENERAL: General questions or conversations

Respond with just the category name, nothing else.

Step 3: Create the Router Logic

  1. Add an “IF” node after the OpenAI response
  2. Set conditions for each category (EMAIL, CALENDAR, TASK, GENERAL)
  3. This will route requests to specialized workflows

The beauty of this setup is that your assistant gets smarter over time. You can add new categories and actions without rebuilding everything from scratch.

Creating Smart Email Management {#email-management}

Email automation is where most people see immediate value. My assistant now handles about 70% of my routine email tasks without any input from me.

Smart Email Responses:

  1. Add a “Gmail” node for the EMAIL route
  2. Configure it to search for unread emails
  3. Connect another “OpenAI” node with this system prompt:
You are drafting email responses. Keep them:
- Professional but friendly
- Concise (under 100 words)
- Action-oriented when needed
- Personalized to the sender

If the email requires personal attention or decision-making, respond with "NEEDS_HUMAN_REVIEW" instead.

Email Filtering and Labeling:

This workflow automatically categorizes incoming emails:

  1. Set up a Gmail trigger for new emails
  2. Use OpenAI to analyze the content and assign categories
  3. Apply Gmail labels automatically
  4. Send urgent emails to Slack for immediate attention

The time-saver here is enormous. Instead of manually sorting through 50+ daily emails, I now check a curated list of emails that actually need my attention.

Meeting Request Automation:

One of my favorite features handles meeting requests automatically:

  1. Detect calendar invites in emails
  2. Check your calendar for conflicts
  3. Accept or propose alternative times
  4. Send confirmation emails

It’s like having a personal secretary who never sleeps and never double-books you.

Adding Calendar Intelligence {#calendar-intelligence}

Calendar management was my biggest pain point. Between client calls, personal appointments, and random “quick chats” that somehow got scheduled, my calendar looked like a game of Tetris gone wrong.

Smart Scheduling:

  1. Create a new workflow triggered by webhook
  2. Add Google Calendar nodes to check availability
  3. Use OpenAI to understand natural language requests like “Schedule coffee with Sarah next Tuesday morning”
  4. Automatically find the best available slot
  5. Send calendar invites and confirmation emails

The natural language processing is surprisingly good. I can say “Block out Thursday afternoon for deep work” and it understands I want a 4-hour block, not a 30-minute meeting.

Buffer Time Management:

This feature automatically adds buffer time between meetings:

  1. Monitor calendar for new events
  2. Check if there’s adequate travel/prep time
  3. Automatically add 15-minute buffers
  4. Send warnings if your day is packed too tight

Weekly Schedule Optimization:

Every Sunday, my assistant analyzes the upcoming week and sends suggestions:

  • “You have 6 hours of meetings on Tuesday, consider moving the non-urgent ones”
  • “Wednesday afternoon is completely free, perfect for that project work”
  • “Don’t forget to block lunch time on Thursday”

It’s like having a time management consultant who actually knows your schedule and preferences.

Task Management Integration {#task-management}

I use Notion for task management, but this works equally well with Todoist, Asana, or any other tool with an API.

Smart Task Creation:

  1. Natural language input: “Remind me to follow up on the Johnson proposal next Friday”
  2. OpenAI extracts:
    • Task: Follow up on Johnson proposal
    • Due date: Next Friday
    • Priority: Based on context
    • Project: Automatically categorized

Intelligent Task Prioritization:

The system analyzes your tasks weekly and provides priority suggestions:

System prompt: "Analyze these tasks and rank them by:
1. Deadline urgency
2. Impact on goals
3. Dependencies on other people
4. Estimated time to complete

Provide a prioritized list with reasoning."

Follow-up Automation:

This is where the assistant really shines. It tracks tasks that depend on other people and automatically sends follow-up reminders:

  1. Identify tasks waiting on external responses
  2. Set automatic follow-up dates
  3. Draft and send polite reminder emails
  4. Escalate if no response after multiple attempts

I went from manually tracking dozens of follow-ups to having zero things fall through the cracks.

Testing and Fine-Tuning {#testing-and-fine-tuning}

The first version of my assistant was… let’s call it enthusiastic. It sent follow-up emails to my mom about dinner plans and tried to schedule meetings at 2 AM. Here’s how I fixed those quirks:

Start Small and Iterate:

I made the mistake of trying to automate everything at once. Instead:

  1. Start with one workflow (I recommend email management)
  2. Test it thoroughly for a week
  3. Fix any issues before adding more features
  4. Gradually expand functionality

Create Safety Checks:

Add conditional nodes that prevent embarrassing mistakes:

  • Don’t send emails between 10 PM and 8 AM
  • Always confirm before scheduling meetings with VIPs
  • Require approval for emails containing certain keywords
  • Set spending limits for any integrations that cost money

Monitor and Adjust:

Set up a simple dashboard to track:

  • How many tasks were automated vs. required human intervention
  • Response accuracy for different types of requests
  • Time saved per week
  • Any recurring errors or edge cases

After two months of tweaking, my assistant now handles about 80% of routine tasks with minimal oversight.

What I Learned (And Mistakes to Avoid) {#what-i-learned}

The Good Surprises:

My assistant became genuinely useful way faster than expected. Within the first week, it was already saving me 2-3 hours of administrative work daily. The natural language processing is impressively good at understanding context and intent.

The unexpected benefit? It made me more organized. When you have to explain your workflow to an AI, you naturally optimize and clarify your own processes.

The Frustrations:

API rate limits hit me hard initially. OpenAI’s free tier runs out quickly if you’re processing lots of emails. Budget about $20-30/month for API calls once you’re fully operational.

The biggest headache was handling edge cases. What happens when someone replies to an AI-generated email? How do you prevent infinite loops? These scenarios require careful planning and testing.

Mistakes That Cost Me Time:

  1. Not testing webhook security: My first assistant responded to any POST request, including spam. Add authentication tokens.

  2. Over-engineering the first version: I spent days building complex logic that handled every possible scenario. Start simple and add complexity as needed.

  3. Ignoring error handling: When an API is down or rate-limited, your workflows break silently. Add proper error notifications and fallback options.

  4. Not backing up workflows: I accidentally deleted a complex workflow and had to rebuild it from scratch. Export your workflows regularly.

The Reality Check:

This isn’t a magic solution that eliminates all work. It’s more like a really good intern who handles the boring stuff so you can focus on what matters. Some tasks still need human judgment, creativity, or empathy.

But for routine administrative work? It’s genuinely life-changing. I now have about 15 extra hours per week to focus on actual productive work instead of email ping-pong and calendar Tetris.

You might also find this useful: How I Built My First AI Agent in 90 Minutes with Flowise (Complete Beginner Tutorial 2026)

You might also find this useful: How I Built My First AI Agent with n8n in 45 Minutes (Complete Beginner Tutorial 2026)

You might also find this useful: How I Built My First AI Agent in 2 Hours (Complete Beginner’s Guide 2026)

Conclusion

Building a personal AI assistant with n8n isn’t just possible in 2026, it’s practical and affordable. The combination of no-code workflow building and powerful AI models means you can create something genuinely useful without hiring a development team.

Start with one workflow that solves your biggest daily frustration. For most people, that’s email management or calendar scheduling. Get that working smoothly, then gradually add more capabilities.

The time investment upfront (about 10-15 hours total) pays dividends quickly. I’m now saving 15+ hours per week on routine tasks, and the assistant keeps getting smarter as I add new workflows and training data.

Ready to build your own AI assistant? Start with the email management workflow above, then let me know in the comments what other tasks you’d like to automate. I’m always curious to see how others customize their digital helpers.

Frequently Asked Questions

How much does it cost to run an AI assistant with n8n?n8n itself is free for self-hosted use. The main costs are AI API calls (about $20-30/month for OpenAI) and any paid integrations you use. Total monthly cost is typically under $50 for heavy usage.

Do I need coding experience to build this?No coding required! n8n uses a visual drag-and-drop interface. However, basic understanding of logic (if-then statements) and APIs is helpful. Most people can build useful workflows within a few hours.

Can I use this assistant on my phone?Yes, through webhooks and integrations. You can send requests via text, email, or messaging apps like Slack. The workflows run on your server and respond wherever you set them up to.

What happens if n8n or OpenAI goes down?Your workflows will pause until services are restored. I recommend setting up error notifications and having backup plans for critical tasks. You can also use multiple AI providers for redundancy.

Is my data secure with this setup?If you self-host n8n, your workflows stay on your server. However, data does pass through third-party APIs (OpenAI, Gmail, etc.) during processing. Review each service’s privacy policy and use encryption where possible.

Cursor Hit 1 Million Daily Users. What Are They Doing Right?

A code editor built by a startup with fewer than 100 employees just hit 1 million daily active users. Stripe uses it. Figma uses it. Over 50,000 businesses have adopted it. In a market where VS Code has owned the editor space for years and JetBrains has loyal enterprise customers, Cursor somehow carved out a massive niche in under two years.

I’ve been trying to figure out what they’re doing differently. Here’s what I think is actually going on.

They Didn’t Build an Extension — They Built an Editor

This is the decision that mattered most. When GitHub Copilot launched, it was a VS Code extension. When other AI coding tools showed up — Tabnine, Codeium, Sourcegraph Cody — they were all extensions too. They had to work within VS Code’s extension API, which limits how deeply you can integrate with the editing experience.

Cursor forked VS Code. They took the entire editor and modified it at the source level. This means they control the rendering pipeline, the file system interactions, the tab management, everything. AI isn’t bolted onto the editor — it’s woven into every interaction.

The difference shows up in small but important ways. When Cursor suggests a multi-file edit, it can show inline diffs across multiple tabs simultaneously. It can modify the file tree. It can run terminal commands and parse the output. An extension can’t do most of this.

// In a VS Code extension, you're limited to the Extension API:
vscode.workspace.applyEdit(workspaceEdit);  // basic text edits
vscode.window.showInformationMessage(msg);   // simple notifications

// Cursor modifies the editor core directly:
// - Custom diff rendering across split panes
// - Inline code generation with real-time syntax highlighting
// - Terminal integration that feeds output back to the AI
// - File tree modifications as part of AI suggestions

The tradeoff is real — Cursor users can’t use every VS Code extension seamlessly, and updates lag behind VS Code mainline. But for the AI-first workflow, the depth of integration is worth it.

Composer 2 Changed the Game

Cursor’s Composer feature, launched in its second major iteration earlier this year, is what turned the editor from “Copilot but nicer” into something genuinely different. Composer lets you describe a change in natural language and the AI implements it across your entire codebase.

But here’s the key: it doesn’t just generate code and dump it in your files. It shows you a plan, lets you approve or modify individual steps, and applies changes incrementally. You stay in control but the AI does the tedious work.

I tested it on a real task last week — adding pagination to a REST API that had nine endpoints, each needing cursor-based pagination with consistent error handling. In the old world, that’s an hour of repetitive work. With Composer 2, I described the pattern once, reviewed the first endpoint’s implementation, approved it, and Composer applied the same pattern to the remaining eight endpoints with appropriate modifications for each.

# I described: "Add cursor-based pagination to all list endpoints.
# Use the same pattern as the users endpoint but adapt for each resource."

# Composer 2 generated changes across 9 files:
# routes/users.py    - already had pagination (used as reference)
# routes/orders.py   - added cursor param, modified query, added next_cursor
# routes/products.py - same pattern, adapted for product-specific sorting
# routes/reviews.py  - same pattern, handled nested relationships
# ... 5 more files

# Each file got contextually appropriate modifications,
# not just find-and-replace templating.
# The orders endpoint correctly paginated by created_at DESC,
# while products used a composite cursor of (category, id).

That’s not autocomplete. That’s codebase-aware transformation. And it works well enough that developers trust it with real production code.

The Pricing Is Aggressive

Cursor Pro costs $20/month. For that you get unlimited AI completions, 500 “fast” requests per month to frontier models (GPT-4, Claude), and unlimited “slow” requests. The free tier gives you 2,000 completions per month — enough for a casual user to get hooked.

Compare that to GitHub Copilot at $10/month for just completions, or Copilot with the chat features at $10/month but with less capable editing integration. Cursor costs more but delivers more.

The 50,000 business customers are on Cursor Business at $40/user/month, which adds team features, admin controls, and privacy guarantees. At enterprise scale, $40/user/month for a tool that measurably increases developer productivity is easy to justify. Engineering time is expensive.

What Keeps Users Stuck

I talked to a dozen developers who switched to Cursor in the last six months. The pattern is consistent: they tried it for a week, and now they can’t go back.

The reason isn’t any single feature. It’s the compound effect of small improvements. Tab completion that understands your current change intent. A chat panel that has full context of your project structure. Multi-file diffs that render cleanly. Terminal output that automatically feeds back into the AI’s understanding.

Each of these is maybe 5% better than the alternative. But they stack. And because they’re integrated at the editor level rather than the extension level, they feel seamless rather than bolted on.

The network effect is kicking in too. When your team uses Cursor, you share .cursorrules files that configure the AI’s behavior for your project. These accumulate project-specific knowledge — coding conventions, architecture patterns, testing approaches — that make the AI better for everyone on the team.

Where Cursor Struggles

It’s not all roses. The VS Code fork means Cursor is perpetually playing catch-up with VS Code updates. Some extensions don’t work correctly. Remote development support is improving but still behind VS Code’s native implementation.

Performance can be an issue on large projects. The indexing that enables Cursor’s codebase awareness takes time and memory. On a monorepo with millions of lines of code, the initial index can take 15-20 minutes and consume several gigabytes of RAM.

And the elephant in the room: vendor lock-in. If Cursor gets acquired, changes pricing dramatically, or shuts down, you’ve built your workflow around a tool you can’t easily replace. Your .cursorrules files, your muscle memory, your team’s processes — all tied to one startup.

The Bigger Picture

Cursor’s success tells us something about where developer tools are heading. The IDE isn’t just a text editor anymore — it’s an AI orchestration layer. The value isn’t in syntax highlighting or git integration. It’s in how deeply the AI understands your codebase and how seamlessly it can modify it.

A million daily users in a market this competitive means Cursor found something real. Whether they keep the lead as Microsoft, Google, and JetBrains pour resources into their own AI integrations is the billion-dollar question. But right now, they’re executing better than companies with a hundred times their resources.

That should make every developer tool company nervous.

Human-in-the-Loop Agent Systems in Java

Fully autonomous agents make great demos. In production, someone on your team will eventually ask: “Can a human check this before it goes out?”

The answer should be yes, and it shouldn’t require bolting on a custom approval system. Human-in-the-loop isn’t a limitation of agent systems — it’s a feature. The best agent architectures make it easy to insert human judgment at exactly the right points, without breaking the execution flow.

This post covers how AgentEnsemble handles human review: the review handler API, review policies, pre-flight validation, and the patterns that make this work in practice.

Why Human-in-the-Loop?

Three reasons show up repeatedly in production agent deployments:

  1. Quality assurance. LLMs produce plausible-sounding output that’s sometimes wrong. A human reviewer catches factual errors, hallucinations, and tone problems that automated checks miss.

  2. Compliance. Regulated industries (finance, healthcare, legal) often require human approval before AI-generated content is used in customer-facing contexts. It’s not optional.

  3. Calibration. When you deploy a new agent workflow, you want to review the first few outputs to verify the agents are behaving as expected before letting them run autonomously.

The Review Handler

The core abstraction is reviewHandler() — a function that receives a task’s output and returns a Review decision:

Ensemble.builder()
    .agents(researcher, writer)
    .tasks(researchTask, writeTask)
    .chatLanguageModel(model)
    .reviewHandler(taskOutput -> {
        System.out.println("=== REVIEW REQUIRED ===");
        System.out.println("Task: " + taskOutput.getTaskDescription());
        System.out.println("Agent: " + taskOutput.getAgentRole());
        System.out.println("Output:n" + taskOutput.getRaw());
        System.out.println("========================");
        System.out.print("Decision (approve/reject/edit): ");

        String decision = scanner.nextLine().trim().toLowerCase();

        return switch (decision) {
            case "approve" -> Review.approve();
            case "reject" -> {
                System.out.print("Reason: ");
                yield Review.reject(scanner.nextLine());
            }
            case "edit" -> {
                System.out.print("Enter corrected output: ");
                yield Review.edit(scanner.nextLine());
            }
            default -> Review.approve();
        };
    })
    .build()
    .run();

Three possible outcomes:

  • Review.approve() — accept the output as-is. The ensemble continues with the next task.
  • Review.reject(reason) — reject the output. The task is re-executed with the rejection reason fed back to the agent as additional context.
  • Review.edit(correctedOutput) — replace the output with a human-provided correction. The ensemble continues with the edited version.

The review handler is a plain Java function. It can be a console prompt (as above), a REST call to an approval service, a Slack message that blocks until someone responds, or a database write that pauses the ensemble until a flag is flipped.

Review Policies

Not every task needs human review. Review policies control which tasks trigger the handler:

Ensemble.builder()
    .agents(researcher, writer, editor)
    .tasks(researchTask, writeTask, editTask)
    .chatLanguageModel(model)
    .reviewHandler(this::handleReview)
    .reviewPolicy(ReviewPolicy.REVIEW_ALL)
    .build()
    .run();

Available policies:

Policy Behavior
REVIEW_ALL Every task output goes through review. Use for high-stakes workflows.
REVIEW_FAILED Only tasks that failed and were retried, or hit max iterations.
FIRST_TASK_ONLY Review the first task output to calibrate. If approved, the rest run without review.

FIRST_TASK_ONLY is particularly useful during the deployment phase. You review the first output to verify the agents are producing what you expect, then let the pipeline run autonomously for the remaining tasks.

Pre-Flight Validation

Sometimes you want an automated quality check before a human sees the output. The beforeReview() hook runs first:

Ensemble.builder()
    .agents(researcher, writer)
    .tasks(researchTask, writeTask)
    .chatLanguageModel(model)
    .beforeReview(taskOutput -> {
        String raw = taskOutput.getRaw();

        // Automated checks
        if (raw == null || raw.isBlank()) {
            return Review.reject("Empty output");
        }
        if (raw.length() < 200) {
            return Review.reject("Output too short -- minimum 200 characters");
        }
        if (raw.contains("I don't know") || raw.contains("I cannot")) {
            return Review.reject("Agent declined the task");
        }

        // Passed automated checks -- proceed to human review
        return Review.skip();
    })
    .reviewHandler(this::humanReview)
    .reviewPolicy(ReviewPolicy.REVIEW_ALL)
    .build()
    .run();

The flow is:

  1. Task completes.
  2. beforeReview() runs automated checks.

    • If it returns Review.reject(), the task re-executes. No human is bothered.
    • If it returns Review.skip(), the output passes to the human reviewHandler().
    • If it returns Review.approve(), the output is accepted without human review.
  3. reviewHandler() presents the output to a human (if beforeReview didn’t already decide).

This pattern keeps humans focused on judgment calls, not on catching obvious failures that a simple check could handle.

Rejection and Re-Execution

When a review is rejected — whether by beforeReview or the human reviewer — the framework re-executes the task with the rejection reason injected as additional context. The agent sees:

Previous attempt was rejected. Reason: “Output too short — minimum 200 characters”

This gives the agent a chance to correct its approach. It’s not just a retry — the agent has feedback on what went wrong.

You can limit the number of review cycles to prevent infinite loops:

Task criticalTask = Task.builder()
    .description("Write the executive summary")
    .expectedOutput("A concise, accurate summary")
    .agent(writer)
    .maxOutputRetries(3) // max 3 re-executions after rejection
    .build();

If the output is still rejected after 3 attempts, the task fails with a clear error.

Patterns for Production Review Workflows

Pattern 1: Console Review (Development)

Good for testing and debugging:

.reviewHandler(taskOutput -> {
    System.out.println(taskOutput.getRaw());
    System.out.print("Approve? (y/n): ");
    return scanner.nextLine().equals("y")
        ? Review.approve()
        : Review.reject("Rejected by developer");
})

Pattern 2: REST API Review (Production)

Block the ensemble until an external approval system responds:

.reviewHandler(taskOutput -> {
    // Submit for review
    String reviewId = approvalService.submitForReview(
        taskOutput.getTaskDescription(),
        taskOutput.getRaw()
    );

    // Poll until decision is made
    ReviewDecision decision = approvalService.awaitDecision(reviewId);

    return switch (decision.status()) {
        case APPROVED -> Review.approve();
        case REJECTED -> Review.reject(decision.reason());
        case EDITED -> Review.edit(decision.correctedOutput());
    };
})

Pattern 3: Slack/Teams Notification

Send a message and wait for a reaction:

.reviewHandler(taskOutput -> {
    String messageId = slack.postMessage(
        "#agent-reviews",
        formatForSlack(taskOutput)
    );

    // Block until thumbs-up or thumbs-down reaction
    SlackReaction reaction = slack.awaitReaction(messageId,
        Duration.ofMinutes(30));

    return reaction.isPositive()
        ? Review.approve()
        : Review.reject("Rejected via Slack");
})

Pattern 4: Automated-Only Review

Skip the human entirely — use beforeReview for automated quality gates:

.beforeReview(taskOutput -> {
    QualityScore score = qualityChecker.evaluate(taskOutput.getRaw());

    if (score.overall() >= 0.8) {
        return Review.approve(); // good enough, no human needed
    } else if (score.overall() >= 0.5) {
        return Review.skip(); // borderline, send to human
    } else {
        return Review.reject("Quality score too low: " + score.overall());
    }
})
.reviewHandler(this::humanReviewForBorderlineCases)

Pattern 5: Tiered Review by Task

Use task-level configuration to vary review intensity:

// Critical task -- always reviewed
Task customerEmail = Task.builder()
    .description("Draft a response to the customer complaint")
    .expectedOutput("Professional, empathetic email response")
    .agent(writer)
    .build();

// Internal task -- skip review
Task internalSummary = Task.builder()
    .description("Summarize the complaint for internal tracking")
    .expectedOutput("Brief internal summary")
    .agent(writer)
    .build();

Ensemble.builder()
    .agents(writer)
    .tasks(customerEmail, internalSummary)
    .chatLanguageModel(model)
    .reviewHandler(taskOutput -> {
        // Only review customer-facing tasks
        if (taskOutput.getTaskDescription().contains("customer")) {
            return humanReview(taskOutput);
        }
        return Review.approve(); // skip internal tasks
    })
    .reviewPolicy(ReviewPolicy.REVIEW_ALL)
    .build()
    .run();

Combining Review with Other Production Features

Review gates compose naturally with other AgentEnsemble features:

Review + Guardrails

Guardrails catch invalid content at the agent level. Review catches quality issues at the workflow level.

Agent writer = Agent.builder()
    .role("Content Writer")
    .goal("Write marketing copy")
    .outputGuardrail(output -> {
        if (containsPII(output)) {
            return GuardrailResult.reject("Output contains PII");
        }
        return GuardrailResult.accept();
    })
    .build();

Ensemble.builder()
    .agents(writer)
    .tasks(writeTask)
    .chatLanguageModel(model)
    .beforeReview(this::automatedQualityCheck)
    .reviewHandler(this::humanReview)
    .build()
    .run();

The execution flow is: Agent runs -> Guardrail validates -> Pre-flight check -> Human review. Each layer catches different classes of problems.

Review + Callbacks

Track review decisions alongside other execution events:

Ensemble.builder()
    .agents(writer)
    .tasks(writeTask)
    .chatLanguageModel(model)
    .reviewHandler(taskOutput -> {
        Review decision = humanReview(taskOutput);
        auditLog.record(taskOutput.getTaskDescription(),
            decision.getType(), decision.getReason());
        return decision;
    })
    .listener(event -> {
        if (event instanceof TaskCompleteEvent e) {
            metrics.recordTaskCompletion(e);
        }
    })
    .build()
    .run();

Review + Structured Output

Review typed output, not raw strings:

record ProposalDraft(
    String title,
    String executiveSummary,
    List<String> keyPoints,
    double estimatedBudget
) {}

Task proposalTask = Task.builder()
    .description("Draft a project proposal")
    .expectedOutput("Structured proposal")
    .agent(writer)
    .outputType(ProposalDraft.class)
    .build();

Ensemble.builder()
    .agents(writer)
    .tasks(proposalTask)
    .chatLanguageModel(model)
    .reviewHandler(taskOutput -> {
        ProposalDraft draft = taskOutput
            .getStructuredOutput(ProposalDraft.class);

        // Review specific fields
        if (draft.estimatedBudget() > 100_000) {
            return Review.reject("Budget exceeds approval threshold");
        }
        if (draft.keyPoints().size() < 3) {
            return Review.reject("Need at least 3 key points");
        }
        return Review.approve();
    })
    .build()
    .run();

The Design Philosophy

Human-in-the-loop isn’t an escape hatch for when agents fail. It’s a first-class architectural decision. The best agent systems are designed with human review points from the start, not retrofitted when something goes wrong in production.

AgentEnsemble makes this easy by treating review as a builder method, not a separate system. Same API, same execution flow, same observability. A human reviewer is just another step in the pipeline.

Get started:

AgentEnsemble is MIT-licensed and available on GitHub.

How to Use pgvector with Python: A Complete Guide

You’ve decided to use PostgreSQL for your vector embeddings. Smart move. Now you need to wire it up from Python — and if you’ve landed here, you’ve probably already noticed that there are a few different libraries involved, the syntax isn’t immediately obvious, and the official pgvector docs give you the C extension but leave the Python story somewhat scattered.

This guide covers the whole picture: installing the Python client, connecting with both psycopg3 and SQLAlchemy, storing and querying embeddings, building indexes, and wiring it up into a real RAG pipeline. By the end you’ll have a working setup you can actually ship.

What You Need Before You Start

You’ll need:

  • A PostgreSQL database with the vector extension enabled
  • Python 3.8+
  • The pgvector Python package

If you’re running PostgreSQL locally, install the pgvector extension from the pgvector GitHub repo and run CREATE EXTENSION vector;. If you’re using a managed PostgreSQL service, the extension is typically pre-installed — on Rivestack, it’s enabled by default on every database.

Install the Python package:

pip install pgvector

You’ll also want a database driver. The two most common choices for Python are psycopg3 (direct, fast, recommended) and SQLAlchemy (for ORM-based projects). We’ll cover both.

# For psycopg3
pip install "psycopg[binary]"

# For SQLAlchemy
pip install sqlalchemy psycopg2-binary

Setting Up the Vector Extension

Connect to your database and enable the extension if it isn’t already:

CREATE EXTENSION IF NOT EXISTS vector;

Run this once per database. You can verify it’s installed with:

SELECT * FROM pg_extension WHERE extname = 'vector';

Using pgvector with psycopg3

psycopg3 is the modern PostgreSQL driver for Python. It’s faster than psycopg2, has proper async support, and the pgvector Python package integrates with it natively.

Connecting and Registering the Vector Type

import psycopg
from pgvector.psycopg import register_vector

conn = psycopg.connect("postgresql://user:password@localhost:5432/mydb")
register_vector(conn)

The register_vector call is important — it tells psycopg how to serialize and deserialize Python lists/numpy arrays into the vector type PostgreSQL expects. Without it, you’ll get type errors when inserting.

Creating a Table with a Vector Column

with conn.cursor() as cur:
    cur.execute("""
        CREATE TABLE IF NOT EXISTS documents (
            id BIGSERIAL PRIMARY KEY,
            content TEXT NOT NULL,
            embedding VECTOR(1536)
        )
    """)
    conn.commit()

The 1536 dimension matches OpenAI’s text-embedding-3-small model. If you’re using a different model, adjust accordingly:

Model Dimensions
OpenAI text-embedding-3-small 1536
OpenAI text-embedding-3-large 3072
Cohere embed-v4 1024
Google text-embedding-005 768
all-MiniLM-L6-v2 (local) 384

Inserting Embeddings

In practice, you generate embeddings with an API call or a local model, then insert them alongside your content:

from openai import OpenAI

client = OpenAI()

def get_embedding(text: str) -> list[float]:
    response = client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    )
    return response.data[0].embedding

documents = [
    "PostgreSQL is a powerful open-source relational database.",
    "pgvector adds vector similarity search to PostgreSQL.",
    "Python is a high-level programming language.",
]

with conn.cursor() as cur:
    for doc in documents:
        embedding = get_embedding(doc)
        cur.execute(
            "INSERT INTO documents (content, embedding) VALUES (%s, %s)",
            (doc, embedding)
        )
    conn.commit()

pgvector’s psycopg integration accepts plain Python lists directly — no special wrapping needed.

Querying by Similarity

To find the documents most similar to a query, compute the query embedding and use one of pgvector’s distance operators:

  • <=> — cosine distance (most common for text embeddings)
  • <-> — L2 (Euclidean) distance
  • <#> — negative inner product
query = "How does vector search work in PostgreSQL?"
query_embedding = get_embedding(query)

with conn.cursor() as cur:
    cur.execute("""
        SELECT content, 1 - (embedding <=> %s) AS similarity
        FROM documents
        ORDER BY embedding <=> %s
        LIMIT 5
    """, (query_embedding, query_embedding))

    results = cur.fetchall()
    for content, similarity in results:
        print(f"[{similarity:.3f}] {content}")

The <=> operator returns cosine distance (0 = identical, 2 = opposite), so 1 - distance gives you cosine similarity if you want a score between 0 and 1.

Using pgvector with SQLAlchemy

If your project uses SQLAlchemy for ORM models, pgvector has first-class support there too.

from sqlalchemy import create_engine, Column, BigInteger, Text
from sqlalchemy.orm import declarative_base, Session
from pgvector.sqlalchemy import Vector

engine = create_engine("postgresql+psycopg2://user:password@localhost:5432/mydb")
Base = declarative_base()

class Document(Base):
    __tablename__ = "documents"

    id = Column(BigInteger, primary_key=True, autoincrement=True)
    content = Column(Text, nullable=False)
    embedding = Column(Vector(1536))

Base.metadata.create_all(engine)

Inserting with SQLAlchemy:

doc = Document(
    content="pgvector makes PostgreSQL a capable vector store.",
    embedding=get_embedding("pgvector makes PostgreSQL a capable vector store.")
)

with Session(engine) as session:
    session.add(doc)
    session.commit()

Querying with SQLAlchemy — use the l2_distance, cosine_distance, or max_inner_product functions:

from pgvector.sqlalchemy import cosine_distance

query_embedding = get_embedding("vector search with PostgreSQL")

with Session(engine) as session:
    results = (
        session.query(Document)
        .order_by(cosine_distance(Document.embedding, query_embedding))
        .limit(5)
        .all()
    )
    for doc in results:
        print(doc.content)

Adding an Index for Production

The queries above work fine for development and small datasets — PostgreSQL does an exact nearest-neighbor scan. But with more than ~50,000 vectors, you’ll want an approximate nearest-neighbor (ANN) index to keep queries fast.

pgvector supports two index types:

  • HNSW — faster queries, higher memory usage during build, better recall
  • IVFFlat — faster build, less memory, slightly lower recall

For most production use cases, HNSW is the right choice:

with conn.cursor() as cur:
    cur.execute("""
        CREATE INDEX IF NOT EXISTS documents_embedding_idx
        ON documents
        USING hnsw (embedding vector_cosine_ops)
        WITH (m = 16, ef_construction = 64)
    """)
    conn.commit()

The vector_cosine_ops tells pgvector to optimize the index for cosine distance — make sure this matches the operator you use in queries (<=>). If you’re using L2 distance, use vector_l2_ops instead.

Important: Build the index after loading your initial data. Building on an empty table and then loading data works too, but index quality is better when built on the full dataset.

HNSW parameters

  • m — number of connections per layer (default 16, range 2–100). Higher = better recall, more memory.
  • ef_construction — search depth during build (default 64, range 4–1000). Higher = better recall, slower build.

For query-time speed/recall tradeoff, set hnsw.ef_search:

with conn.cursor() as cur:
    cur.execute("SET hnsw.ef_search = 100")
    # Now run your similarity query

Building a RAG Pipeline with pgvector and Python

Here’s a minimal but complete Retrieval-Augmented Generation pipeline using everything above:

import psycopg
from pgvector.psycopg import register_vector
from openai import OpenAI

openai_client = OpenAI()
db = psycopg.connect("postgresql://user:password@localhost:5432/mydb")
register_vector(db)

def embed(text: str) -> list[float]:
    return openai_client.embeddings.create(
        model="text-embedding-3-small",
        input=text
    ).data[0].embedding

def index_document(content: str):
    """Store a document and its embedding."""
    embedding = embed(content)
    with db.cursor() as cur:
        cur.execute(
            "INSERT INTO documents (content, embedding) VALUES (%s, %s)",
            (content, embedding)
        )
    db.commit()

def retrieve(query: str, k: int = 5) -> list[str]:
    """Find the k most relevant documents for a query."""
    query_embedding = embed(query)
    with db.cursor() as cur:
        cur.execute("""
            SELECT content
            FROM documents
            ORDER BY embedding <=> %s
            LIMIT %s
        """, (query_embedding, k))
        return [row[0] for row in cur.fetchall()]

def answer(question: str) -> str:
    """Retrieve context and generate an answer."""
    context_docs = retrieve(question)
    context = "nn".join(context_docs)

    response = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": "Answer questions using only the provided context. Be concise."
            },
            {
                "role": "user",
                "content": f"Context:n{context}nnQuestion: {question}"
            }
        ]
    )
    return response.choices[0].message.content

# Index some documents
index_document("pgvector supports HNSW and IVFFlat indexes.")
index_document("Cosine similarity is best for text embedding comparisons.")
index_document("Rivestack provides managed PostgreSQL with pgvector pre-installed.")

# Ask a question
print(answer("What index types does pgvector support?"))

Metadata Filtering

One of the big advantages of pgvector over standalone vector databases is that you can filter by regular columns in the same query. If you have multi-tenant data or want to search within a category, just add a WHERE clause:

# Add a source column to your table
cur.execute("""
    ALTER TABLE documents ADD COLUMN IF NOT EXISTS source TEXT
""")

# Query with metadata filter
cur.execute("""
    SELECT content, embedding <=> %s AS distance
    FROM documents
    WHERE source = 'internal-wiki'
    ORDER BY embedding <=> %s
    LIMIT 5
""", (query_embedding, query_embedding))

No syncing, no secondary filtering step, no extra infrastructure. It’s just SQL.

Connection Pooling

pgvector queries are fast, but embedding vectors are large (1536 floats ≈ 6KB per vector). If you’re running a web application with concurrent requests, use a connection pool to avoid exhausting PostgreSQL’s connection limit:

from psycopg_pool import ConnectionPool

pool = ConnectionPool("postgresql://user:password@localhost:5432/mydb",
                      min_size=2, max_size=10,
                      configure=register_vector)

with pool.connection() as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT content FROM documents ORDER BY embedding <=> %s LIMIT 5",
                    (query_embedding,))
        results = cur.fetchall()

On Rivestack, connection pooling is built into the platform so you don’t need to manage PgBouncer yourself — you connect to the pooler endpoint and the rest is handled for you.

Async Support

If you’re building with FastAPI, asyncio, or any other async Python framework, psycopg3 has full async support:

import asyncio
import psycopg
from pgvector.psycopg import register_vector_async

async def main():
    conn = await psycopg.AsyncConnection.connect(
        "postgresql://user:password@localhost:5432/mydb"
    )
    await register_vector_async(conn)

    query_embedding = get_embedding("vector search")
    async with conn.cursor() as cur:
        await cur.execute("""
            SELECT content FROM documents
            ORDER BY embedding <=> %s LIMIT 5
        """, (query_embedding,))
        results = await cur.fetchall()
        for row in results:
            print(row[0])

asyncio.run(main())

Note the register_vector_async — you need the async version when working with AsyncConnection.

Common Mistakes

Using the wrong distance operator for your index. If you create an HNSW index with vector_cosine_ops but query with <-> (L2 distance), the index won’t be used and you’ll get a full sequential scan. Match the operator to the index ops class.

Forgetting register_vector. You’ll get can't adapt type 'list' errors. Call it once per connection (or once on the pool).

Building indexes on empty tables. The index will exist but won’t actually improve query quality. Load your data first.

Not setting ef_search for precision-sensitive use cases. The default is 40, which is fast but sacrifices some recall. For RAG where missing a relevant document matters, SET hnsw.ef_search = 100 is worth the small latency increase.

Where to Go From Here

You now have everything you need to build vector search into a Python application using pgvector:

  • psycopg3 for direct, fast database access
  • SQLAlchemy if you prefer an ORM
  • HNSW indexes for production performance
  • Metadata filtering using standard SQL WHERE clauses
  • A full RAG pipeline skeleton you can extend

The operational side — backups, connection pooling, high availability, keeping pgvector updated — is where the real work begins if you’re self-hosting. If you’d rather not deal with that, try Rivestack: managed PostgreSQL with pgvector pre-installed, NVMe storage for fast index traversal, and automatic backups. You get the same SQL interface you’ve just built, without the 3 AM pages.

Originally published at rivestack.io

Dejé de hacer vibe coding. Este es el framework que construí para reemplazarlo.

El problema que nadie quiere admitir

Llevas meses usando IA para codear. Claude, Copilot, Cursor — lo que sea. Y funciona. Las primeras 2 horas van genial.

Después pasan cosas como estas:

  • Le pides que implemente un servicio y te escribe // TODO: implement later
  • Le pides tests y los escribe después de ver el código — básicamente confirma lo que ya hizo
  • Le preguntas si funciona y te dice “sí, debería funcionar” — sin evidencia, sin tests, sin nada
  • Cierras la sesión, vuelves al día siguiente, y Claude no recuerda nada de lo que decidieron juntos

Eso es vibe coding. Y es el enemigo del software de calidad.

La pregunta incómoda

¿Cuándo fue la última vez que tu IA escribió un test que falló primero?

Si la respuesta es “nunca” o “no recuerdo”, entonces tus tests no están validando comportamiento. Están confirmando implementación. Es como calificar tu propio examen — siempre vas a pasar.

Lo que construí

Después de meses de frustración, construí Don Cheli — un framework de Desarrollo Dirigido por Especificaciones (SDD) que transforma el caos asistido por IA en ingeniería de software real.

La premisa es simple: no puedes escribir código sin haber escrito el test primero. Y no puedes escribir el test sin haber definido la especificación.

Spec → Test → Code. En ese orden. Sin excepciones.

Las 3 Leyes de Hierro

Llamo “leyes de hierro” a las reglas que el framework no permite saltarse bajo ninguna circunstancia:

1. TDD Obligatorio

No es una recomendación. No es un “best practice” que se ignora cuando hay prisa. Es una restricción dura en el pipeline.

/dc:start “Implementar autenticación JWT”

Se genera spec Gherkin con criterios de aceptación
Se escriben tests DESDE la spec (la IA NO ha visto código aún)
Los tests fallan (RED) ← esto es correcto
Se escribe el código mínimo para pasar (GREEN)
Se refactoriza (REFACTOR)

La diferencia clave: cuando la IA escribe tests después del código, los tests están acoplados a la implementación. Cambias la implementación → los tests se rompen aunque el comportamiento sea correcto. Con spec-first, los tests validan comportamiento, no detalles de implementación.

2. Debugging: causa raíz primero

Nada de “prueba cambiando esto a ver si funciona”. El framework fuerza un proceso:

Reproducir → Aislar → Entender → Corregir → Verificar

3. Verificación: evidencia antes de afirmaciones

“Los tests pasan” > “creo que funciona”. Si no hay evidencia, no se avanza.

Pre-mortem: anticipar el fracaso antes de codear

Esta es la feature que más me gusta y que ningún otro framework tiene.

/razonar:pre-mortem “Migración de PostgreSQL a MongoDB”

La IA imagina que tu proyecto ya falló. Analiza por qué falló. Y te da un plan para prevenirlo. Todo esto antes de escribir una sola línea de código.

Don Cheli tiene 15 modelos de razonamiento:

Modelo Para qué
Pre-mortem Anticipar fracaso
5 Porqués Encontrar la causa raíz real
Pareto Enfocarse en el 20% que produce el 80%
Inversión ¿Cómo garantizaríamos el fracaso?
Primeros principios Descomponer al fundamento
Segundo orden Consecuencias de las consecuencias
Costo de oportunidad ¿Qué pierdo por NO elegir la otra opción?
Reversibilidad ¿Puedo deshacerlo? Calibrar nivel de compromiso
Minimizar arrepentimiento ¿Qué decisión lamentaré menos en 10 años?
Círculo de competencia ¿Esto lo conozco de verdad o solo creo que lo conozco?
Mapa-territorio ¿Mi modelo refleja la realidad?
Probabilístico Asignar probabilidades en vez de certezas
RLM (3 modelos) Razonamiento con sub-LLMs para problemas complejos

Ningún otro framework de IA tiene esto. GSD no lo tiene. BMAD no lo tiene. Spec Kit no lo tiene.

Estimación: nadie más lo hace

/dc:estimate docs/prd.md

4 modelos que se complementan:

Modelo Técnica
Puntos de Función Complejidad funcional
Planning Poker IA 3 agentes estiman independientemente
COCOMO Líneas de código estimadas → esfuerzo
Histórico Comparación con tareas similares

El resultado es un rango: estimado optimista, esperado y pesimista con desglose por feature. Pregunté a cada framework competidor si pueden estimar esfuerzo. La respuesta: ninguno.

*## Debate adversarial: roles que se enfrentan
*

/dc:debate “¿Monolito o microservicios para el MVP?”

Esto invoca roles senior que deben encontrar problemas con las propuestas de los demás:

  • CPO: “El monolito nos deja lanzar 2-3x más rápido”
  • Arquitecto: “Pero sin boundaries, la extracción futura será un rewrite”
  • QA: “Monolito es más fácil de testear end-to-end”
  • Seguridad: “Un solo deploy = una sola superficie de ataque… pero también un solo punto de falla”

La regla: “no tengo objeciones” no es una respuesta válida. Cada rol DEBE encontrar al menos un problema. Esto saca a la luz riesgos que un solo agente nunca genera.

## Mesa técnica: expertos con +15 años

/dc:tech-panel “¿Redis o Memcached para caching de sesiones?”

5 expertos senior debaten tu decisión técnica:

  • Tech Lead — Visión holística, impacto en velocity del equipo
  • Sr. Backend — Patrones de resiliencia, connection pooling, race conditions
  • Sr. Frontend — Core Web Vitals, SSR vs CSR, design systems
  • Arquitecto — System design, C4 diagrams, ADRs
  • Sr. DevOps/SRE — IaC, SLOs/SLIs, zero-downtime deploys

Cada uno respalda sus argumentos con incidentes reales, benchmarks o post-mortems públicos. Argumentos sin respaldo se marcan como [teórico] y tienen menor peso.

Auditoría OWASP integrada

/dc:security-audit

Escanea las 10 categorías de OWASP:

  • A01 Broken Access Control — endpoints sin auth, IDOR, CORS
  • A02 Cryptographic Failures — passwords en plaintext, JWT sin expiración
  • A03 Injection — SQL, XSS, command injection
  • A04-A10 — Configuración, componentes vulnerables, logging

Cada hallazgo incluye: severidad, archivo, línea y fix sugerido. No es un plugin externo — está integrado en el pipeline.

El pipeline: 6 pasos, 6 puertas de calidad


mermaid
flowchart LR
    A["SPECIFY"] -->|Gate 1| B["CLARIFY"]
    B -->|Gates 2+3| C["PLAN"]
    C -->|Gate 4| D["BREAKDOWN"]
    D -->|Gate 5| E["IMPLEMENT"]
    E -->|Gate 6| F["REVIEW"]
    F -->|Fail| E
Cada puerta bloquea el avance si no se cumplen los criterios. No hay atajos.

Pero — y esto es importante — no necesitas memorizar nada de esto. Solo ejecutas:


/dc:start "Implementar autenticación JWT"
El framework detecta la complejidad automáticamente y ejecuta el pipeline apropiado. Para tareas simples, es ligero. Para tareas complejas, es exhaustivo. Se adapta.

Comparación honesta con otros frameworks
BMAD    GSD GSD-2   spec-kit    Don Cheli
Stars   42K 39K 3K  82K Nuevo
Comandos    ~20 ~80 ~40 ~10 72+
Skills  ~15 ~15 ~10 ~6  43
Razonamiento    — — — — 15 modelos
Estimación — — — — 4 modelos
Quality gates   — 1   2   4   6
TDD obligatorio — — — — Ley de hierro
OWASP   — — — — Integrado
i18n    — — — — EN/ES/PT
Multi-plataforma    — — — — Claude, Cursor, Antigravity

**Sí, tenemos menos stars. Somos nuevos. Pero en features, ninguno se acerca.**

Multilenguaje real (no solo READMEs traducidos)
Al instalar, eliges tu idioma. Todo el framework se adapta:

Nombres de carpetas: habilidades/ (ES) → skills/ (EN)
Nombres de comandos: /dc:iniciar (ES) → /dc:init (EN) → /dc:iniciar (PT)
Contenido de reglas: traducido a tu idioma
CLAUDE.md: en tu idioma
.cursorrules: en tu idioma

No es un README traducido con los archivos en otro idioma. Es i18n real, todo para que entiendas la estructura, entiendas los comandos y empieces a codear de verdad sin ninguna limitante de idioma o conocimientos avanzados.

Multi-plataforma
Don Cheli funciona con:

Plataforma  Soporte
Claude Code Completo — 72+ comandos, 43 skills, 7 agentes
Google Antigravity  14 skills + 9 workflows + router a 43 habilidades (Aqui no pudo ampliarlo mas pero es un buen nivel de avance)
Cursor  .cursorrules completo (115 líneas) con todos los comandos
Codex/Otros Instrucciones genéricas vía AGENTS.md

**Instalación en 1 minuto
**

curl -fsSL https://raw.githubusercontent.com/doncheli/don-cheli-sdd/main/scripts/instalar.sh | bash -s -- --global --lang es
O si prefieres verificar antes de ejecutar:


curl -fsSL https://raw.githubusercontent.com/doncheli/don-cheli-sdd/main/scripts/instalar.sh -o instalar.sh
cat instalar.sh  # Revisa antes de ejecutar
bash instalar.sh --global
Después, en cualquier proyecto:


/dc:start "Tu tarea aquí"

**Eso es todo. No necesitas leer documentación de 50 páginas. El framework te guía.**

Open source
Apache 2.0. Gratis. Sin tiers pagos. Sin limitaciones.

GitHub: github.com/doncheli/don-cheli-sdd
npm: npx don-cheli-sdd init
Web: doncheli.tv

72 comandos. 43 habilidades. 15 modelos de razonamiento. 4 de estimación. 6 puertas de calidad. TDD como ley de hierro.

Hecho en Latinoamérica para el mundo, si te gusta apoyame con un Star en Github, para poder expandir el framework a muchas mas personas. 

Deja de adivinar. Empieza a hacer ingeniería.


MSW Has a Free API Mocking Framework — Stop Using Custom Mock Servers

You’re probably mocking APIs wrong. Custom mock servers, intercepting fetch globally, hardcoding responses — all fragile, all pain.

MSW (Mock Service Worker) fixes this at the network level.

What is MSW?

MSW intercepts HTTP requests at the network level using Service Workers (browser) or custom interceptors (Node.js). Your application code doesn’t know it’s being mocked. That’s the point.

Why MSW Changes Everything

1. Network-Level Interception

import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]);
  }),

  http.post('/api/users', async ({ request }) => {
    const user = await request.json();
    return HttpResponse.json({ id: 3, ...user }, { status: 201 });
  }),
];

Your fetch(), axios, ky — all intercepted. No code changes needed.

2. Same Mocks for Browser AND Node.js

// Browser (Storybook, dev server)
import { setupWorker } from 'msw/browser';
const worker = setupWorker(...handlers);
worker.start();

// Node.js (tests, SSR)
import { setupServer } from 'msw/node';
const server = setupServer(...handlers);
server.listen();

Write mocks once. Use everywhere.

3. Request Handlers That Feel Natural

import { http, HttpResponse } from 'msw';

const handlers = [
  // Path parameters
  http.get('/api/users/:id', ({ params }) => {
    return HttpResponse.json({ id: params.id, name: 'Alice' });
  }),

  // Query parameters
  http.get('/api/search', ({ request }) => {
    const url = new URL(request.url);
    const query = url.searchParams.get('q');
    return HttpResponse.json({ results: [`Result for ${query}`] });
  }),

  // Error responses
  http.get('/api/protected', () => {
    return HttpResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }),

  // Network errors
  http.get('/api/unstable', () => {
    return HttpResponse.error();
  }),
];

4. Perfect for Testing

import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('shows user profile', async () => {
  // Override for this specific test
  server.use(
    http.get('/api/users/1', () => {
      return HttpResponse.json({ id: 1, name: 'Test User', role: 'admin' });
    })
  );

  render(<UserProfile userId="1" />);
  expect(await screen.findByText('Test User')).toBeInTheDocument();
});

test('handles API errors gracefully', async () => {
  server.use(
    http.get('/api/users/1', () => {
      return HttpResponse.json({ error: 'Not found' }, { status: 404 });
    })
  );

  render(<UserProfile userId="1" />);
  expect(await screen.findByText('User not found')).toBeInTheDocument();
});

5. WebSocket Mocking

import { ws } from 'msw';

const chat = ws.link('wss://api.example.com/chat');

export const handlers = [
  chat.addEventListener('connection', ({ client }) => {
    client.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));

    client.addEventListener('message', (event) => {
      client.send(JSON.stringify({
        type: 'echo',
        data: event.data
      }));
    });
  }),
];

MSW vs Other Mocking Approaches

MSW Jest mocks Nock JSON Server
Level Network Module HTTP Server
Browser Yes No No Yes
Node.js Yes Yes Yes Yes
Framework agnostic Yes Jest only Node only Yes
WebSocket Yes No No No
No code changes Yes Requires import changes Yes Yes

Getting Started

npm install msw --save-dev
npx msw init public/ --save

Create src/mocks/handlers.ts, define your handlers, and you’re done.

The Bottom Line

MSW is the standard for API mocking in 2026. Network-level interception, cross-environment support, and WebSocket mocking make it the tool your test suite has been missing.

Building data-intensive applications? I create custom web scraping and data extraction tools. Check out my Apify actors or email [email protected].

We analyzed 10,000 voice AI calls. The LLM was rarely the problem.

We built Dograh OSS, an open-source voice AI platform. When we started, we assumed most failures would come from the LLM – bad answers, missed intent, prompt edge cases. So we spent a lot of early effort there.

Then we looked at the data. We ran automated QA where an LLM reviews every turn in every call and tags what went right and wrong, and we spent hours listening to calls ourselves. Across roughly 10,000 calls spanning customer support, appointment booking, and lead qualification, the failure picture looked nothing like what we expected.

The problems that showed up again and again were about the phone call as a medium. Timing, audio physics, and infrastructure designed decades before LLMs existed.

Here is what we found, roughly ranked by frequency.

Failure area Share Primary driver
STT / word error rate ~38% Low-quality telephony audio and accent variation
First-8-second chaos ~34% Greeting latency, barge-in, variable user behavior
Interruption handling ~28% Filler words breaking flow, context switching
Extended silence ~22% Users distracted, fetching info, handing off phone
Tool call latency ~19% LLM turns plus external API latency
LLM failure modes ~15% Hallucinations, instruction drift, latency trade-offs
Broken escalation ~11% No clear human handoff path

These categories overlap. A lot of bad calls had two or three of these compounding each other.

STT is worse than you think

STT failures were the single biggest contributor to broken calls, and the one most consistently underestimated by teams building voice AI for the first time.

Standard telephony runs at 8 kHz audio. That is genuinely low quality. It strips away acoustic detail that helps distinguish consonants, so “schedule” becomes “school” in a transcript and “Sunday” comes through as “someday.” Add background noise, speakerphone, or a non-native accent and word error rates climb fast.

The thing about WER is that errors compound. “I need to school my appointment for someday” should be understood as “I need to schedule my appointment for Sunday,” and a well-prompted LLM will figure that out. When three or four words are garbled in the same turn though, contextual inference falls apart. We saw calls where entire turns came through as near-gibberish.

There is also a dimension STT completely misses. Transcription captures words but it does not capture how those words are said. A frustrated “fine” and a genuinely agreeable “fine” are two very different things, but they look identical in a transcript. Tone helps disambiguate words that sound similar. When a caller sounds confused or hesitant, a human listener picks up on that and adjusts. STT gives you a flat string of text and the LLM works with it blind.

No STT provider has uniform accuracy across all accents either. The gap between a provider’s accuracy on American English versus Nigerian English or heavily accented Spanish can be 15 to 25 percentage points. If your users call from diverse regions, picking one STT provider and moving on is going to hurt you.

Two mitigations made a real difference in our testing. First, adding a custom vocabulary to your STT engine – and I mean beyond domain jargon. If you are building an order management bot, add the common words your callers actually say: “order,” “ID,” “payment,” “cancel,” “account,” “address.” Thirty to forty frequently used words. The STT engine listens for these with extra weight and it meaningfully reduces errors on the terms that matter most.

Second, tell the LLM to expect transcription errors. A single line in your system prompt acknowledging that the caller’s words may contain transcription noise, and asking the model to use contextual reasoning before responding, reduces the downstream impact of STT failures significantly. The LLM stops treating garbled transcripts as literal input and starts being smarter about what the caller probably meant.

The first 8 seconds are where calls die

About a third of all problematic calls failed before the real conversation even started.

A lot happens in those first 8 to 10 seconds. Callers are still deciding whether they want to engage. Many have not fully shifted their attention to the call. Some are already talking before the bot finishes its greeting, and others wait much longer than expected, unsure whether the silence means the system is broken. This is also the window where callers most frequently realize they are talking to a bot, and many immediately start testing it – interrupting, asking off-script questions, being deliberately vague. The variance in behavior is just much higher here than at any other point in the call.

Greeting latency makes everything worse. A gap of more than a second or two between the call connecting and the first word of audio is enough for many callers to assume things are broken and hang up. Pre-generating and caching your greeting audio instead of synthesizing it fresh every time removes an entire class of failures here.

What works: keep greetings short, under six seconds. Consider disabling barge-in for just the first 3 to 4 seconds if your greeting contains information the caller needs to hear. Re-enable interruption after that. The period where barge-in causes the most problems is right at the start.

In Dograh, each workflow node has its own “Allow Interruption” toggle, so you can switch off interruption on the starting node for a short introduction and re-enable it for the rest of the conversation.

Interruptions, silence, and dead air

Most barge-in documentation focuses on detecting when a user starts speaking and stopping the TTS output. That part is reasonably well solved. The harder problem is what happens to the conversation after the interruption.

One pattern we saw constantly: a caller says “uh huh” or “yeah” while the bot is talking. The bot interprets this as a genuine turn, stops speaking, and tries to process it as new user input. The LLM produces a response to what is essentially a non-sentence and the conversation breaks. The caller has to re-explain what they wanted.

A related pattern involves context switching – a caller interrupts to ask “wait, does that include weekends?” The bot handles the question fine but loses track of what it was explaining before. The original task gets dropped without resolution.

Both problems are about how interruption events are handled in the conversation state. The fix is designing explicit logic for what happens after an interruption. Differentiate between filler sounds and substantive speech, and track unresolved conversational threads so the bot can come back to them.

Silence is closely related. Real callers go quiet for perfectly legitimate reasons – checking an email for an order ID, handing the phone to someone else, looking something up. A caller fetching information might go quiet for 20 to 40 seconds. Most voice AI systems interpret this as confusion or abandonment and respond with prompts or just hang up.

A graduated response works much better: a brief neutral filler after 5 seconds, a gentle “still there?” check at 15 seconds, a real choice at 45 seconds (“I can hold, or you are welcome to call back”), and a graceful close with callback instructions only after 90 seconds or more.

Tool call latency creates its own kind of dead air

When a voice agent needs to look something up in a CRM or check a calendar slot, a tool call gets triggered. In practice that means at least one additional LLM turn to interpret the result, plus the external API latency. The total gap can easily reach 3 to 5 seconds, and callers at that point genuinely don’t know if the call is still alive.

We are adding a “playback speech” feature in Dograh that lets you configure a pre-recorded audio clip to play while a tool call executes. This fills the silence without the LLM having to generate a response and it keeps the caller engaged. Beyond that, pre-fetching data you know will be needed at call start – account details, prior call history, caller ID lookups – keeps common tool calls out of the live response path entirely.

LLM failures and broken escalation

LLM failures in production voice AI are real but probably smaller than you would expect. Hallucination gets the most attention but it accounts for a smaller share of bad calls than the quieter failures. Models stop following instructions partway through long calls. They generate empty responses that cause the bot to say nothing at all. They produce answers that are technically coherent but completely disconnected from the previous turn.

The trade-off between model size and latency matters here too. Smaller models respond faster, which helps perceived call quality, but they follow complex instructions less reliably. Larger models handle nuanced prompts better but their response latency feeds right back into the dead-air problem.

Escalation failures have a lower frequency but they are the most consequential category on this list. The callers asking for a human almost always have the hardest, most urgent problems. They have already tried self-service and it didn’t work. When the bot can’t detect they want to escalate – because they phrased it in a way the intent detection doesn’t recognize – or when the escalation path drops the context so the human agent starts from scratch, that caller’s experience is about as bad as it gets.

Escalation should be a first-class destination in every workflow. The phrases callers use to ask for a human are wildly more varied than any training set anticipates, and the transfer needs to carry the full conversation transcript to the receiving agent.

The pattern

Voice AI breaks at transitions. The first seconds of a call, the moment a user interrupts, the gap while a tool is running, the point where a caller needs a human. These are the edges where the system’s assumptions about how conversations work meet how people actually behave on phone calls.

Teams that focus almost entirely on LLM response quality and treat these transitions as secondary concerns tend to ship agents that sound good in demos but disappoint in production. The calls that held up well in our data were the ones with the most deliberate handling of everything that happens between LLM responses.

We are building Dograh as the open-source alternative to Vapi, Retell, and Bland. No per-minute fees, no vendor lock-in, deploy on your own infra. Ask anny queries

Star us on GitHub | Try the cloud version

Never Used RSS? Start Here.

person reading on laptop with coffee in calm workspace

You open your favorite app to catch up on the news. Thirty minutes later, you’ve watched two unrelated videos, scrolled past a dozen ads, and still haven’t read the article your coworker mentioned. Sound familiar?

Algorithmic feeds are designed to maximize engagement, not to inform you. They decide what you see, when you see it, and how long you stay. The result is a reading experience that feels busy but leaves you less informed. There’s a better way, and it’s been around for over two decades.

RSS — Really Simple Syndication — is a quiet technology that puts you back in control. No algorithms. No ads. No tracking. Just the content you asked for, delivered the moment it’s published. If you’ve never tried it, this guide will have you set up in under ten minutes.

person reading on laptop with coffee in calm workspace

Photo by ubeyonroad on Unsplash

What Is RSS, Actually?

RSS stands for Really Simple Syndication. It’s an open standard that lets websites publish updates in a structured format — a small file that lists the latest articles, posts, or episodes. When you subscribe to a site’s RSS feed, your reader checks that file periodically and shows you anything new.

Think of it like email subscriptions, but without the email. You don’t hand over your address. You don’t get marketing messages. You just see the content. According to Wikipedia’s overview of RSS, the format dates back to 1999 and remains one of the most widely supported syndication standards on the web.

Most websites you already read have an RSS feed. News outlets, blogs, magazines, government sites, YouTube channels, podcasts, and even some social media accounts publish feeds. You just need something to read them with.

The magic of RSS is its simplicity. There’s no company controlling the pipe between you and the publisher. No one is inserting sponsored content or reordering your timeline to boost engagement metrics. What gets published is what you get. In that order. Every time.

Why RSS Deserves a Comeback

You might wonder why you’d bother with a technology from the early 2000s. The answer is that the problems RSS solves have only gotten worse.

Social platforms increasingly gate content behind recommendation engines. A study published by the Reuters Institute for the Study of Journalism found that a growing share of people feel they have little control over the news they encounter online. Algorithmic curation means you might miss important reporting from a source you follow simply because the platform decided something else would get more clicks.

RSS eliminates that problem entirely. Every article, every post, every update shows up in your reader chronologically. You choose your sources. You see everything they publish. You decide what to read and what to skip.

It’s also private

When you subscribe to an RSS feed, the publisher doesn’t know who you are. There’s no account, no tracking pixel, no behavioral profile. Your reading habits stay yours. In a decade defined by data privacy concerns, that matters more than most people realize.

And it’s fast

No ads loading. No cookie banners. No autoplay videos. RSS delivers clean text and images. Your reader renders content instantly, which means you spend time reading instead of waiting.

Setting Up Your First Feed Reader

Getting started with RSS takes about five minutes. Here’s the process, step by step.

Step 1: Choose a feed reader

A feed reader is the app that collects and displays your RSS subscriptions. There are many options — web-based, desktop, and mobile. Some popular choices in 2026 include Feedly, Inoreader, NetNewsWire (free and open source for Mac and iOS), and Miniflux for self-hosters.

EchoLive also includes a full-featured feeds inbox with support for up to 100 free feeds, auto-refresh, keyboard shortcuts, and built-in audio generation. If you want to read and listen to your feeds in one place, it’s worth a look.

Pick whatever fits your workflow. The beauty of RSS is that you’re never locked in — your subscriptions are portable.

Step 2: Find feeds to subscribe to

Most websites have an RSS feed even if they don’t advertise it. Here’s how to find them:

  • Look for the RSS icon. Some sites display an orange RSS icon, usually in the header or footer.
  • Check the URL. Try adding /feed, /rss, or /atom.xml to the end of a website’s address. For example, example.com/feed often works for WordPress sites.
  • Use your reader’s search. Most feed readers let you paste a website URL and will auto-detect the feed.
  • Try a feed discovery tool. EchoLive offers semantic search and a curated category browser to help you find feeds by topic. Describe what you’re interested in, and subscribe in seconds.

Start small. Pick five to ten sources you genuinely want to follow — your favorite news outlet, a blog you always come back to, maybe a niche publication in your field.

Step 3: Organize with folders or tags

Once you’ve added a handful of feeds, group them. Most readers support folders or categories. A simple structure works best:

  • News — Major outlets and breaking coverage
  • Tech — Industry blogs and product updates
  • Personal — Friends’ blogs, hobby sites, creative writing
  • Work — Industry publications, company blogs, research

Don’t over-organize at the start. You can always restructure later as your collection grows.

Step 4: Set a reading rhythm

RSS works best as a daily habit, not a firehose. Check your reader once or twice a day. Scan headlines. Open what interests you. Mark the rest as read and move on. The goal isn’t to read everything — it’s to have a curated source of content that respects your attention.

Building a Feed Collection That Works

The biggest mistake new RSS users make is subscribing to too many feeds at once. You end up with hundreds of unread items, feel overwhelmed, and quit. Here’s how to avoid that.

Start with ten feeds or fewer

Quality over quantity. Choose sources where you’d read most of what they publish. If a site posts twenty articles a day and you only care about one, that feed will generate noise. Look for publications with a manageable posting frequency — a few articles per week is ideal for most personal readers.

Prune regularly

Every few weeks, glance at your feed list. If you’re consistently skipping a source, unsubscribe. RSS should feel like a curated magazine, not an obligation. There’s no penalty for removing a feed, and you can always add it back.

Mix your sources

A good feed collection balances breadth and depth. Include a major news outlet for general awareness, niche blogs for deep expertise, and maybe a few unexpected sources — a webcomic, a cooking blog, a local government feed — to keep things interesting.

Import existing subscriptions

If you’re switching from another reader or already have a list of feeds, most readers support OPML — a standard file format for feed lists. You can import OPML files into virtually any reader and have your entire collection set up instantly.

Going Further: Audio, Discovery, and Daily Habits

Once you’ve settled into RSS, you might want to go beyond reading. Here are a few ways to level up.

Listen to your feeds

Not every article needs your full visual attention. Commuting, exercising, or cooking are perfect times to catch up on your feeds by listening instead of reading. You can turn RSS into audio using text-to-speech tools that convert articles into natural-sounding audio. EchoLive’s feeds inbox, for example, offers built-in audio generation with 630+ neural voices — pick a voice, hit play, and follow along with word-level sync highlighting.

Build a daily brief

Instead of checking feeds manually, some tools can compile a curated audio briefing from your subscriptions. EchoLive’s Daily Brief combines your feeds and trending stories into a scored, skippable audio digest with transcript view and date navigation. It’s like having a personal news anchor who only covers topics you care about.

Explore trending content alongside your feeds

RSS is great for following specific sources, but sometimes you want to discover what’s happening more broadly. EchoLive’s Pulse feature surfaces real-time trending stories from across the web, consolidated by topic. It’s a nice complement to your curated feed list — algorithmic discovery on your terms, not a platform’s.

Share what you find

One of the underrated joys of RSS is stumbling on articles worth sharing. Save the best pieces to collections, highlight key passages, and share them with friends or colleagues. Good content is meant to be passed along.

Take Control of What You Read

RSS isn’t complicated. It isn’t outdated. It’s a quiet, powerful tool that gives you something increasingly rare in 2026 — full control over your information diet. No algorithm stands between you and the sources you trust.

Start with five feeds today. Give it a week. You’ll wonder how you ever relied on a timeline that someone else curated for you. And if you want to read, listen, and organize your feeds in one place, EchoLive is a good place to start.

Originally published on EchoLive.

I got tired of ad-filled, fake-button TikTok Downloaders, so I built my own (Minimalist & Ad-Free)

I got tired of ad-filled, fake-button TikTok Downloaders, so I built my own (Minimalist & Ad-Free)

Let’s be honest. Have you ever tried searching for a “TikTok Video Downloader” on Google?

The search results are a UX nightmare. You click on a link, and you are immediately bombarded with:

  • 3 different popup windows.
  • 5 buttons that say “DOWNLOAD NOW” (4 of which are malware ads).
  • A blindingly bright, cluttered interface from 2010.

As a web developer, this frustrated me. The internet doesn’t need another spammy download site. It needs tools that respect the user’s time and eyes.

So, I spent my recent weekends building TikSpider.

What is TikSpider?

TikSpider.com is a blazing fast, web-based tool to download TikTok videos without watermarks, extract audio (MP3), and save TikTok Stories.

But more importantly, it’s an experiment in bringing the “Less is More” design philosophy into a space that is notoriously chaotic.

🎨 The Design Philosophy: Dark Mode & Frosted Glass

Most utility tools overlook design. I wanted to build something that feels like a native, premium app rather than a sketchy script.

I stripped away everything unnecessary. No sidebars, no sticky banner ads, no confusing navigation. Just a single, perfectly centered input field.

To make it easy on the eyes (especially for late-night scrollers), the entire UI is built around a pure Dark Mode aesthetic. I used frosted glass (glassmorphism) effects for the main interactive panels. The result is a UI that feels cold, sharp, and highly technical—exactly what a “spider” fetching data should feel like.

⚙️ Under the Hood & Features

Building this wasn’t just about making it look pretty; it had to be functional and fast.

Here is what it currently handles perfectly:

  1. No Watermark Video Extraction: It parses the official URL and fetches the clean, unwatermarked HD MP4 stream directly.
  2. TikTok to MP3: A lot of users just want the trending audio. The tool isolates and extracts the audio track instantly.
  3. TikTok Stories: Stories disappear in 24 hours. TikSpider bypasses the app restrictions to let you save them locally.
  4. Photo Slideshows: It also cleanly extracts HD images from TikTok photo swipe-posts.

🌍 The Next Challenge: Global i18n and SEO Architecture

One of the most interesting technical challenges I’m tackling right now is scaling this globally. TikTok is massive in regions like Indonesia, Latin America, and Europe.

Instead of relying on auto-translate plugins, I’m building a robust hreflang based multi-language architecture (deploying native sub-directories like /es/, /id/, etc.) to serve localized versions without compromising the core Next.js/Nuxt caching performance or the minimalist UI layout.

🚀 Try it out and roast my UI!

I built this tool for users who appreciate clean, efficient software. It’s completely free, requires no installation, and will remain ad-free in its core downloading experience.

Check it out here: TikSpider.com

I would absolutely love the Dev.to community’s feedback.

  • How does the frosted glass dark UI feel on your device?
  • Did it parse your links fast enough?
  • Any edge cases where the spider failed to fetch?

Drop a comment below. I’ll be reading and replying to everything! 🕷️