---
name: azion-pause-an-agent-for-approval-with-kv-store
description: >-
  Run a tool-calling agent loop in a function, pause it in KV Store before an action a person approves, and record every task event in SQL Database.
---

# Pause an agent for approval with KV Store

You run a tool-calling agent loop in a function, store a task in KV Store when the model asks for an action a person must approve, resume it on the decision, and write every task event to SQL Database, with the Azion CLI and a function's code.

---

## Prerequisites

- An application and a workload that serve your domain, with **Application Accelerator** turned on, which the **Run Function** behavior requires. To create them, refer to [Applications quickstart](/en/documentation/platform/applications/quickstart/).
- KV Store and SQL Database enabled on the account. Both are in Preview and not enabled by default, so request access through [Technical Support](/en/documentation/support/).
- The `agent-state` KV Store namespace. To create it, refer to [Create the namespace](/en/documentation/guides/application-development/data/deduplicate-webhook-deliveries-with-kv-store/#create-the-namespace).
- The `agent-history` database, with a table created by the statement `CREATE TABLE task_events (task_id TEXT NOT NULL, event TEXT NOT NULL, steps INTEGER NOT NULL, at TEXT NOT NULL);`. To create the database and send the statement, refer to [Create a database using the API](/en/documentation/guides/application-development/data/manage-sql-database/#create-a-database-using-the-api) and [Create a table using the API](/en/documentation/guides/application-development/data/create-tables-sql-database/#create-a-table-using-the-api).
- A personal token with the **Edit SQL Database** permission, for the history writes. To create one, refer to [Personal tokens](/en/documentation/guides/platform/account-and-billing/personal-tokens/).
- The [Azion CLI](/en/documentation/devtools/cli/), installed and authorized, to store the environment variables.
- A third-party provider whose chat endpoint accepts the OpenAI chat completions format with tool definitions, with its URL, the name of a model that supports tool calling, and an API key.

The examples give the agent two tools over an order service at `https://api.example.com/v1`: `get_order`, which reads `GET /orders/{id}`, and `refund_order`, which calls `POST /orders/{id}/refunds` and needs a person's approval. They use `www.example.com` for the domain. Replace them with your values.

---

## Store the values the function reads

The provider key, the backend token, the approver secret, and the personal token stay out of the code, as environment variables.

To store the values the function reads, run these commands with the Azion CLI. A key that contains `key`, `token`, or `secret` is stored as a secret by default:

```bash
azion create variables --key "PROVIDER_URL" --value "<provider-chat-completions-url>" --secret false
azion create variables --key "PROVIDER_MODEL" --value "<provider-model-name>" --secret false
azion create variables --key "PROVIDER_API_KEY" --value "<provider-api-key>"
azion create variables --key "BACKEND_API_TOKEN" --value "<backend-token>"
azion create variables --key "APPROVER_SECRET" --value "<approver-secret>"
azion create variables --key "AZION_TOKEN" --value "[TOKEN VALUE]"
azion create variables --key "SQL_DATABASE_ID" --value "<database-id>" --secret false
```

The account holds the seven variables the agent function reads with `Azion.env.get()`.

The [Build AI agents](/en/documentation/use-cases/build-and-run-ai-workloads/build-ai-agents/) use case uses the values of this example.

---

## Create the agent function

The function runs the loop on `/api/agent/tasks` and resumes it on `/api/agent/approve`. A tool in `APPROVAL_TOOLS` never runs from the loop: the function stores the task under `task:<task-id>` in `agent-state` for one day, and runs the tool only when a person approves it.

Create a function named `ops-agent` with this code. The provider call sends the key as `Authorization: Bearer`; change that header to the one your provider requires:

```javascript
const MAX_STEPS = 6;
const API_BASE = "https://api.example.com/v1";
const APPROVAL_TOOLS = new Set(["refund_order"]);
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const SYSTEM_PROMPT =
  "You are an operations agent for an order service. Use the tools to complete the task. " +
  "Answer with a short summary once the task is done.";

const TOOLS = [
  { "type": "function", "function": {
    "name": "get_order",
    "description": "Get one order by its ID, with its status and total.",
    "parameters": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] }
  } },
  { "type": "function", "function": {
    "name": "refund_order",
    "description": "Refund an order in full. A person approves every refund before it runs.",
    "parameters": { "type": "object", "properties": { "order_id": { "type": "string" }, "reason": { "type": "string" } }, "required": ["order_id", "reason"] }
  } }
];

async function runTool(name, args) {
  const headers = {
    "Authorization": `Bearer ${Azion.env.get("BACKEND_API_TOKEN")}`,
    "Content-Type": "application/json"
  };
  const order = encodeURIComponent(args.order_id ?? "");
  let response;
  if (name === "get_order") {
    response = await fetch(`${API_BASE}/orders/${order}`, { headers });
  } else if (name === "refund_order") {
    response = await fetch(`${API_BASE}/orders/${order}/refunds`, {
      method: "POST", headers, body: JSON.stringify({ reason: args.reason })
    });
  } else {
    return { error: `unknown tool ${name}` };
  }
  return response.ok ? await response.json() : { error: `the order service answered ${response.status}` };
}

async function callModel(messages) {
  const response = await fetch(Azion.env.get("PROVIDER_URL"), {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${Azion.env.get("PROVIDER_API_KEY")}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({ model: Azion.env.get("PROVIDER_MODEL"), messages, tools: TOOLS })
  });
  if (!response.ok) throw new Error(`provider answered ${response.status}`);
  const body = await response.json();
  return body?.choices?.[0]?.message;
}

async function record(taskId, event, steps) {
  const statement =
    `INSERT INTO task_events (task_id, event, steps, at) VALUES ('${taskId}', '${event}', ${steps}, datetime('now'));`;
  const response = await fetch(
    `https://api.azion.com/v4/workspace/sql/databases/${Azion.env.get("SQL_DATABASE_ID")}/query`,
    {
      method: "POST",
      headers: {
        "Accept": "application/json",
        "Authorization": `Token ${Azion.env.get("AZION_TOKEN")}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ statements: [statement] })
    }
  );
  const body = await response.json();
  const failed = (body.data ?? []).find((entry) => entry.error);
  if (!response.ok || failed) {
    console.log(JSON.stringify({ event: "history_write_failed", taskId, error: failed?.error ?? response.status }));
  }
}

async function runLoop(kv, taskId, state) {
  while (state.steps < MAX_STEPS) {
    state.steps++;
    const message = await callModel(state.messages);
    const calls = message?.tool_calls ?? [];
    if (calls.length === 0) {
      await record(taskId, "completed", state.steps);
      return { task_id: taskId, status: "completed", answer: message?.content ?? "", steps: state.steps };
    }
    state.messages.push({
      role: "assistant",
      content: message.content || `Calling ${calls.map((c) => c.function?.name).join(", ")}.`
    });
    for (const call of calls) {
      const name = call.function?.name;
      const raw = call.function?.arguments ?? {};
      const args = typeof raw === "string" ? JSON.parse(raw) : raw;
      if (APPROVAL_TOOLS.has(name)) {
        state.pending = { name, args };
        await kv.put(`task:${taskId}`, state, { expirationTtl: 86400 });
        await record(taskId, "awaiting_approval", state.steps);
        return { task_id: taskId, status: "awaiting_approval", action: state.pending };
      }
      const result = await runTool(name, args);
      state.messages.push({ role: "user", content: `Result of ${name}: ${JSON.stringify(result)}` });
    }
  }
  await record(taskId, "step_limit", state.steps);
  return { task_id: taskId, status: "step_limit", steps: state.steps };
}

export default {
  async fetch(request, env, ctx) {
    if (request.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }
    const url = new URL(request.url);
    const kv = await Azion.KV.open("agent-state");
    let taskId = null;
    try {
      if (url.pathname === "/api/agent/tasks") {
        const { task } = await request.json();
        if (typeof task !== "string" || task.trim() === "") {
          return Response.json({ error: "task is required" }, { status: 400 });
        }
        taskId = crypto.randomUUID();
        await record(taskId, "started", 0);
        const state = {
          steps: 0,
          pending: null,
          messages: [{ role: "system", content: SYSTEM_PROMPT }, { role: "user", content: task }]
        };
        return Response.json(await runLoop(kv, taskId, state));
      }

      if (url.pathname === "/api/agent/approve") {
        if (request.headers.get("Authorization") !== `Bearer ${Azion.env.get("APPROVER_SECRET")}`) {
          return new Response("Unauthorized", { status: 401 });
        }
        const { task_id, approved } = await request.json();
        if (!UUID.test(task_id ?? "")) {
          return Response.json({ error: "task_id is not a task ID" }, { status: 400 });
        }
        taskId = task_id;
        const state = await kv.get(`task:${taskId}`, "json");
        if (!state?.pending) {
          return Response.json({ error: "no pending action for this task" }, { status: 404 });
        }
        const { name, args } = state.pending;
        state.pending = null;
        await kv.put(`task:${taskId}`, state, { expirationTtl: 86400 });
        await record(taskId, approved === true ? "approved" : "rejected", state.steps);
        const result = approved === true ? await runTool(name, args) : { error: "a person rejected this action" };
        state.messages.push({ role: "user", content: `Result of ${name}: ${JSON.stringify(result)}` });
        return Response.json(await runLoop(kv, taskId, state));
      }

      return new Response("Not found", { status: 404 });
    } catch (error) {
      console.log(JSON.stringify({ event: "task_failed", taskId, error: String(error?.message ?? error) }));
      if (taskId) await record(taskId, "failed", 0);
      return Response.json({ task_id: taskId, status: "failed", error: "the task could not continue" }, { status: 502 });
    }
  },
};
```

To create the function and its instance, follow [Functions quickstart](/en/documentation/platform/functions/quickstart/) with the name `ops-agent`, and name the instance `ops-agent`, with no Args.

The application carries an `ops-agent` instance that runs a task until the model answers, a refund needs approval, or six steps pass.

The [Build AI agents](/en/documentation/use-cases/build-and-run-ai-workloads/build-ai-agents/) use case uses the values of this example.

---

## Run the function on the agent's paths

One rule runs the instance on both agent paths. Both take `POST`, and the function answers any other method with `405`.

Create a Request Phase rule as [Create the rule for the path and the method](/en/documentation/guides/application-development/functions-and-runtime/run-a-function-on-one-path-and-roll-it-back/#create-the-rule-for-the-path-and-the-method) describes, with these values: the name `agent - tasks and approvals`, one criteria group with `${uri}` *starts with* `/api/agent/` in place of the guide's path and method groups, and the **Run Function** behavior selecting the `ops-agent` instance.

A task sent to `/api/agent/tasks` runs the agent, and every start, pause, decision, and end lands in `task_events`. A new rule takes a few minutes to propagate.

---

## Confirm a task pauses and resumes once

To send a task that needs only `get_order`:

```bash
curl -s -X POST https://www.example.com/api/agent/tasks \
  -H 'Content-Type: application/json' \
  -d '{"task":"What is the status of order <order-id>?"}'
```

The response carries `"status":"completed"`, an `answer` that states the order's status, and `steps` of 2 or more: one step that calls `get_order`, and one that answers.

Send `{"task":"Refund order <order-id>, the customer received a damaged item."}` to the same path. The response carries `"status":"awaiting_approval"`, the `task_id`, and an `action` naming `refund_order` with the order ID, and the order service has received no refund.

To approve the refund, send the `task_id` from that response with the approver secret:

```bash
curl -s -X POST https://www.example.com/api/agent/approve \
  -H 'Authorization: Bearer <approver-secret>' \
  -H 'Content-Type: application/json' \
  -d '{"task_id":"<task-id>","approved":true}'
```

The response carries `"status":"completed"`, and the order service has received one refund. The same request sent again answers `404` with `no pending action for this task`, and a request without the `Authorization` header answers `401`.

To read the events of the refund task, send the statement `SELECT event, steps FROM task_events WHERE task_id = '<task-id>' ORDER BY rowid;` to the SQL Database API, as [Query rows](/en/documentation/guides/application-development/data/create-tables-sql-database/#query-rows) shows. The `results` rows list `started`, `awaiting_approval`, `approved`, and `completed`, in that order.

The agent pauses before the refund, runs it once after the approval, and records each event in `task_events`.

These checks confirm the [Build AI agents](/en/documentation/use-cases/build-and-run-ai-workloads/build-ai-agents/) use case.

---

## Next steps

- [Write rows to SQL Database from a function](/en/documentation/guides/application-development/data/write-sql-database-rows-from-a-function.md): Store the database credentials and check the result of every statement a function sends.
- [Build AI agents](/en/documentation/use-cases/build-and-run-ai-workloads/build-ai-agents.md): The design this function serves: the step limit, the approval rule, and the stores, with the reason for each.
