Skip to main content
This guide walks through the full flow for letting your users keep the charts an Upsolve agent builds in chat:
  1. Call the chat endpoint from your own UI and capture the generated chart from the stream.
  2. Show a “Save and add to dashboard” button next to the rendered chart.
  3. On click, persist the chart with Save Chart from Chat, then pin it to a dashboard with Add Charts to Dashboard.
Charts the agent creates in chat are ephemeral — they exist only in the response stream and the agent’s session memory. Until you call Save Chart from Chat, nothing is stored, and the chart is gone when the conversation ends.

Prerequisites

  • A project user token for the end user, minted server-side via Get Project User Token — see Backend Setup. All three endpoints in this guide accept it.
  • An agent ID — the agent your users chat with.
  • A dashboard ID — the target dashboard. For a fixed (“hardcoded”) dashboard, copy its ID once from the hub, or look it up with List Workspace Dashboards.

Step 1: Chat and capture the chart

Call the chat endpoint and watch the stream for a tool-result event whose toolName is generateChart. Its payload.result.chartConfig is the chart — keep the whole object in state, untouched.
// See the Chat with an Agent reference for the full streaming parser.
if (
  event.type === "tool-result" &&
  event.payload?.toolName === "generateChart" &&
  event.payload?.result?.chartConfig
) {
  setGeneratedChart({
    chartConfig: event.payload.result.chartConfig, // save this verbatim
    chartData: event.payload.result.chartData, // rows, for rendering a preview
  });
}
Pass chartConfig through exactly as received. It normally includes a sqlOverride field carrying the SQL the agent wrote — that is what the saved chart uses to load data on a dashboard. If your tool result carries the query on the sibling sql field instead of on the config, forward it on the request’s top-level sql field. Without SQL from either source, the save endpoint rejects the request.

Step 2: The “Save and add to dashboard” button

When a chart arrives, render it (using chartData) alongside a save button:
function SaveAndAddToDashboardButton({
  chartConfig,
  agentId,
  dashboardId,
  projectUserToken,
}: {
  chartConfig: object;
  agentId: string;
  dashboardId: string; // your hardcoded target dashboard
  projectUserToken: string;
}) {
  const [state, setState] = useState<"idle" | "saving" | "saved" | "error">(
    "idle"
  );

  const handleClick = async () => {
    setState("saving");
    try {
      // 1. Persist the chart
      const saveRes = await fetch(
        "https://api.upsolve.ai/v1/api/chart/save-from-chat",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${projectUserToken}`,
          },
          body: JSON.stringify({ chartConfig, agentId }),
        }
      );
      const saveJson = await saveRes.json();
      if (saveJson.status !== "success") {
        throw new Error(saveJson.error?.message ?? "Failed to save chart");
      }
      const chartId = saveJson.data.chartId;

      // 2. Add it to the dashboard
      const addRes = await fetch(
        "https://api.upsolve.ai/v1/api/dashboard/add-charts",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${projectUserToken}`,
          },
          body: JSON.stringify({
            targetDashboardId: dashboardId,
            charts: [{ chartId }],
          }),
        }
      );
      const addJson = await addRes.json();
      if (addJson.status !== "success") {
        throw new Error(addJson.error?.message ?? "Failed to add chart");
      }

      setState("saved");
    } catch (err) {
      console.error(err);
      setState("error");
    }
  };

  return (
    <button onClick={handleClick} disabled={state === "saving"}>
      {state === "idle" && "Save and add to dashboard"}
      {state === "saving" && "Saving…"}
      {state === "saved" && "Added to dashboard ✓"}
      {state === "error" && "Failed — try again"}
    </button>
  );
}

Step 3 (reference): the two calls, as cURL

Persist the chart:
curl -X POST "https://api.upsolve.ai/v1/api/chart/save-from-chat" \
  -H "Authorization: Bearer <project_user_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "chartConfig": { /* toolResult.result.chartConfig, verbatim */ },
    "agentId": "<agent_id>"
  }'
{
  "status": "success",
  "data": {
    "chartId": "b3f7…",
    "name": "Monthly Revenue 2025"
  }
}
Add it to the dashboard:
curl -X POST "https://api.upsolve.ai/v1/api/dashboard/add-charts" \
  -H "Authorization: Bearer <project_user_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "targetDashboardId": "<dashboard_id>",
    "charts": [{ "chartId": "b3f7…" }]
  }'
The chart appears appended below the dashboard’s existing charts, full width. Users can resize and reposition it from the dashboard UI afterwards.

How the pieces fit

ConcernBehavior
Why agentId?The save endpoint links the chart to the agent’s data connection and project, so the chart can query data when rendered on any dashboard. Recommended — pass the same ID you chat with.
Deep copyAdd Charts to Dashboard copies the saved chart into the dashboard (new chart ID in the response’s addedChartIds). The original stays in the chart library and can be added to other dashboards too.
Edit behaviorSaved chat charts are marked AI-generated; in the Upsolve hub their “Edit” action reopens the agent chat rather than the manual chart editor.
Repeat clicksEach click saves a new chart and adds a new tile. Disable the button after success (as above) unless you want duplicates.
AuthBoth endpoints also accept an Upsolve API key (Authorization: Bearer up_…) for server-side flows — e.g. if you proxy the save through your backend.

Troubleshooting

You extracted a partial config (for example, re-built the object from the rendered chart) instead of forwarding toolResult.result.chartConfig verbatim, and no query was supplied on the top-level sql field. Forward the config as received, or pass the tool call’s sql on the request body.
The agentId belongs to a different organization than the token you are calling with, or the ID is not the agent’s ID (check the agent URL in the hub).
You passed both agentId and an explicit connectionId, but the connection lives in a different project than the agent. Pass a connectionId from the agent’s project, or omit it to use the agent’s own connection.
The chart needs a connection to run its SQL on a dashboard. Pass agentId (preferred — its data model supplies the connection) or an explicit connectionId. If you pass an agentId whose data model has no connection, attach a connection to the data model or pass connectionId directly.