Articles

Let an AI SDK Agent Write and Run Weather Code

Use Vercel AI SDK tool calling with Verklet so an agent can write JavaScript, call Open-Meteo, run the code in the browser, and promote to the server only when needed.

Most tool-calling demos hide the interesting work inside the tool. The model asks for weather, the app calls a getWeather function, and the agent receives a tidy JSON object.

That is useful, but it is not the same as giving the agent a runtime. If the agent can write files, run commands, inspect stdout, fix errors, and try again, the tool stops being a single integration. It becomes a code workspace.

That distinction matters when you are building with the Vercel AI SDK. AI SDK gives you the agent loop and the tool-calling surface. Verklet gives the tool a filesystem, package installs, process execution, and a hybrid backend that starts in the browser and promotes to a server when the workload needs it.

The example in this article is deliberately small: ask an agent for the weather, let it write JavaScript that calls the open and free Open-Meteo weather API, run that code, and explain the result.

The shape of the integration

The host app gives the model one generic capability:

runCode({
  files: {
    'weather.mjs': '...',
  },
  command: 'node',
  args: ['weather.mjs'],
});

The model decides what code to write. For the weather question, it can call Open-Meteo directly because the API is public and does not require an application-specific secret for a basic forecast request.

That is the important design choice. You are not building a bespoke weather tool. You are building a code execution tool that happens to be able to use an open weather API.

Boot a runtime for the conversation

Create one runtime per chat, task, or user workspace. With backend: 'auto', Verklet starts in the browser for the work that fits there.

import { Runtime } from '@verklet/sdk';

const runtime = await Runtime.boot({
  projectId: 'prj_your_project_id',
  backend: 'auto',
  persistenceKey: `agent:${chatId}`,
});

The browser path is the fast path. JavaScript execution, supported Python, filesystem edits, package hydration, and isolated previews can run in the tab. If the agent later asks for a command that needs Linux, native Python wheels, uv, or another server-only capability, Verklet can snapshot the same workspace and continue on the server backend.

The app still calls runtime.fs and runtime.spawn(). The backend changes because the workload needs it, not because the product had to switch to a second integration.

Add a code runner tool

In AI SDK, a tool has an input schema and an execute function. The execute function is where the host app bridges from model intent to Verklet process execution.

import { Runtime } from '@verklet/sdk';
import { tool } from 'ai';
import { z } from 'zod';

type RuntimeHandle = Awaited<ReturnType<typeof Runtime.boot>>;

export function createRunCodeTool(runtime: RuntimeHandle) {
  return tool({
    description:
      'Write files into a temporary workspace and run a command. Use this to execute JavaScript that fetches public data, parses it, and prints a clear result.',
    inputSchema: z.object({
      files: z.record(z.string(), z.string()),
      command: z.enum(['node', 'python']),
      args: z.array(z.string()).default([]),
    }),
    execute: async ({ files, command, args }) => {
      for (const [path, contents] of Object.entries(files)) {
        await runtime.fs.writeFile(`/workdir/${path}`, contents);
      }

      const proc = await runtime.spawn(command, args, { cwd: '/workdir' });
      const [stdout, stderr, exitCode] = await Promise.all([
        readStream(proc.output),
        readStream(proc.stderr),
        proc.exit,
      ]);

      return {
        backend: runtime.capabilities.backend ?? 'browser',
        exitCode,
        stdout: stdout.slice(0, 12_000),
        stderr: stderr.slice(0, 12_000),
      };
    },
  });
}

async function readStream(stream: ReadableStream<string>) {
  let output = '';
  for await (const chunk of stream) output += chunk;
  return output;
}

That is the whole bridge: mount files, spawn a process, collect output, return it to the model.

In a production app, you would add stricter limits around filenames, process lifetime, total file size, stdout size, and allowed commands. The article example keeps the boundary visible.

Let the agent call Open-Meteo from code

Now wire the tool into an AI SDK call. The prompt should be explicit about the behavior you want: write code, run it, inspect the result, and only answer after a successful execution.

import { generateText, isStepCount } from 'ai';
import { openai } from '@ai-sdk/openai';

const result = await generateText({
  model: openai('gpt-4o'),
  stopWhen: isStepCount(6),
  tools: {
    runCode: createRunCodeTool(runtime),
  },
  system: [
    'You are a coding agent with access to a real runtime.',
    'When the user asks for current weather, write JavaScript that calls Open-Meteo.',
    'Use fetch, print JSON or a concise text summary, then run the file with node.',
    'If execution fails, fix the code and run it again before answering.',
  ].join('\n'),
  prompt:
    'What is the weather in Oslo right now? Write code to fetch it, run the code, and explain the result.',
});

console.log(result.text);

The agent can write a file like this:

const latitude = 59.91;
const longitude = 10.75;
const url = new URL('https://api.open-meteo.com/v1/forecast');

url.searchParams.set('latitude', String(latitude));
url.searchParams.set('longitude', String(longitude));
url.searchParams.set('current', 'temperature_2m,wind_speed_10m,weather_code');

const response = await fetch(url);
if (!response.ok) {
  throw new Error(`Open-Meteo returned ${response.status}`);
}

const data = await response.json();
console.log(JSON.stringify(data.current, null, 2));

Then it runs:

node weather.mjs

The output goes back to the model as tool result data. The final answer can be grounded in what the code actually printed, instead of a hallucinated weather report.

Why this is better than a weather tool

A weather-specific tool is still useful when your product has one narrow job. But a coding agent usually needs a more general loop:

Open-Meteo is just the proving ground. The same runCode tool can let an agent call a public API, transform CSV, generate an SVG, validate a JSON payload with zod, or test a tiny reproduction before responding.

That is the product shift: AI SDK decides the next step, and Verklet executes the step in a real workspace.

Browser first, server when needed

For this weather example, the runtime can stay in the browser. The agent writes weather.mjs, runs node, receives stdout, and answers. There is no need to create a container or server workspace just to fetch a public JSON endpoint.

The same integration still has a server path. If the agent changes the task to use a native Python package, a subprocess-heavy script, uv, or a toolchain that the browser cannot run, backend: 'auto' lets Verklet promote the workspace:

The AI SDK tool does not need to know whether the process ran in the tab or after promotion. It can return runtime.capabilities.backend so the agent or UI can explain what happened when that distinction matters.

The safety boundary

The generic tool is powerful, so the host app owns the policy:

Those limits are easier to reason about when all execution passes through one tool boundary. The model can be creative inside the workspace, while the product controls what that workspace is allowed to do.

The short version

Do not make the weather API the tool. Make code execution the tool.

With Vercel AI SDK, the model can decide to write and run a program. With Verklet, that program starts in the browser, gets a real filesystem and process API, and can promote to the server only when the workload needs server capabilities.

That is enough to turn a chat answer into a small, verifiable coding loop: write the weather script, run it, inspect the result, and answer from the evidence.