Flutter and Node.
The Stack Selection
I didn't want a clunky Python script running in a terminal; I needed something that felt like actual software.
- Flutter: This handles the entire frontend. I specifically used Flutter for desktop (Windows/macOS) because having a native app to trigger and monitor my AI workflows is far more efficient than a web tab.
- Node.js: This acts as the orchestration layer. It manages the state, handles the API handshakes with the LLM, and controls the browser.
- Playwright: I swapped out Puppeteer for Playwright because it's significantly more stable with modern, dynamic web apps and has better auto-waiting mechanisms.
How the AI Workflow Actually Functions
The secret to making this work is splitting the LLM's brain into two roles: the Planner and the Actuator. If you just ask an LLM to "do the task," it hallucinate selectors and crashes.
1. The Intent: I send a command from the Flutter UI (e.g., "Find the last three invoices from Client X and summarize them").
2. The Planner: Node.js sends this to the LLM. The LLM doesn't write code yet; it creates a high-level roadmap. It identifies the sequence: Navigate → Search → Extract → Summarize.
3. The Execution Loop: This is where the real-world deployment gets tricky. Node.js triggers Playwright to perform the first step.
4. The Actuator (Observation): After every single action, the system scrapes the current page state (the DOM or a simplified version of it) and feeds it back to the LLM. The LLM then decides the exact next click or keystroke based on what it actually sees on the screen, not what it thinks should be there.
5. Completion: Once the goal is flagged as complete, the result is pushed back to the Flutter app.
Technical Implementation Details
For those looking for a practical tutorial on the backend side, your Node.js setup needs to be lean. I used Express for the API and the official SDKs for the LLM integration.
// Example of how the Node.js orchestrator handles the Playwright loop
async function executeAgentTask(goal) {
const browser = await playwright.chromium.launch();
const page = await browser.newPage();
let taskCompleted = false;
while (!taskCompleted) {
const pageState = await page.content();
const nextAction = await llm.decideNextStep(goal, pageState);
if (nextAction.type === 'DONE') {
taskCompleted = true;
} else {
await performAction(page, nextAction);
}
}
await browser.close();
}The result? My daily busywork dropped by about 60%. The biggest win wasn't just the time saved, but the mental energy recovered from not having to do the same five clicks a hundred times a day. The cross-platform nature of Flutter means I can trigger these agents from my desktop and just let them run in the background while I focus on actual deep work.