Narrative Agent
curl --request POST \
--url https://agents.graphy.dev/api/v0/narrate \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"config": {
"type": "line",
"data": {
"columns": [
{
"key": "month",
"label": "Month"
},
{
"key": "users",
"label": "Active Users"
}
],
"rows": [
{
"month": "Jan",
"users": 1200
},
{
"month": "Feb",
"users": 1800
},
{
"month": "Mar",
"users": 3100
}
]
}
},
"userPrompt": "Write a punchy title and a one-line caption",
"metadata": {
"callId": "req-narrate-1",
"locale": "EN_US"
}
}
'import requests
url = "https://agents.graphy.dev/api/v0/narrate"
payload = {
"config": {
"type": "line",
"data": {
"columns": [
{
"key": "month",
"label": "Month"
},
{
"key": "users",
"label": "Active Users"
}
],
"rows": [
{
"month": "Jan",
"users": 1200
},
{
"month": "Feb",
"users": 1800
},
{
"month": "Mar",
"users": 3100
}
]
}
},
"userPrompt": "Write a punchy title and a one-line caption",
"metadata": {
"callId": "req-narrate-1",
"locale": "EN_US"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
config: {
type: 'line',
data: {
columns: [{key: 'month', label: 'Month'}, {key: 'users', label: 'Active Users'}],
rows: [
{month: 'Jan', users: 1200},
{month: 'Feb', users: 1800},
{month: 'Mar', users: 3100}
]
}
},
userPrompt: 'Write a punchy title and a one-line caption',
metadata: {callId: 'req-narrate-1', locale: 'EN_US'}
})
};
fetch('https://agents.graphy.dev/api/v0/narrate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://agents.graphy.dev/api/v0/narrate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'config' => [
'type' => 'line',
'data' => [
'columns' => [
[
'key' => 'month',
'label' => 'Month'
],
[
'key' => 'users',
'label' => 'Active Users'
]
],
'rows' => [
[
'month' => 'Jan',
'users' => 1200
],
[
'month' => 'Feb',
'users' => 1800
],
[
'month' => 'Mar',
'users' => 3100
]
]
]
],
'userPrompt' => 'Write a punchy title and a one-line caption',
'metadata' => [
'callId' => 'req-narrate-1',
'locale' => 'EN_US'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://agents.graphy.dev/api/v0/narrate"
payload := strings.NewReader("{\n \"config\": {\n \"type\": \"line\",\n \"data\": {\n \"columns\": [\n {\n \"key\": \"month\",\n \"label\": \"Month\"\n },\n {\n \"key\": \"users\",\n \"label\": \"Active Users\"\n }\n ],\n \"rows\": [\n {\n \"month\": \"Jan\",\n \"users\": 1200\n },\n {\n \"month\": \"Feb\",\n \"users\": 1800\n },\n {\n \"month\": \"Mar\",\n \"users\": 3100\n }\n ]\n }\n },\n \"userPrompt\": \"Write a punchy title and a one-line caption\",\n \"metadata\": {\n \"callId\": \"req-narrate-1\",\n \"locale\": \"EN_US\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://agents.graphy.dev/api/v0/narrate")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"config\": {\n \"type\": \"line\",\n \"data\": {\n \"columns\": [\n {\n \"key\": \"month\",\n \"label\": \"Month\"\n },\n {\n \"key\": \"users\",\n \"label\": \"Active Users\"\n }\n ],\n \"rows\": [\n {\n \"month\": \"Jan\",\n \"users\": 1200\n },\n {\n \"month\": \"Feb\",\n \"users\": 1800\n },\n {\n \"month\": \"Mar\",\n \"users\": 3100\n }\n ]\n }\n },\n \"userPrompt\": \"Write a punchy title and a one-line caption\",\n \"metadata\": {\n \"callId\": \"req-narrate-1\",\n \"locale\": \"EN_US\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://agents.graphy.dev/api/v0/narrate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"config\": {\n \"type\": \"line\",\n \"data\": {\n \"columns\": [\n {\n \"key\": \"month\",\n \"label\": \"Month\"\n },\n {\n \"key\": \"users\",\n \"label\": \"Active Users\"\n }\n ],\n \"rows\": [\n {\n \"month\": \"Jan\",\n \"users\": 1200\n },\n {\n \"month\": \"Feb\",\n \"users\": 1800\n },\n {\n \"month\": \"Mar\",\n \"users\": 3100\n }\n ]\n }\n },\n \"userPrompt\": \"Write a punchy title and a one-line caption\",\n \"metadata\": {\n \"callId\": \"req-narrate-1\",\n \"locale\": \"EN_US\"\n }\n}"
response = http.request(request)
puts response.read_body{
"message": "<string>"
}REST API
Narrative Agent
Generate a chart’s title, subtitle, and caption. Narrative is written into config.content as TipTap JSON. Requires userPrompt.
The response is a Server-Sent Events (SSE) stream with progress, complete, and error events.
POST
/
api
/
v0
/
narrate
Narrative Agent
curl --request POST \
--url https://agents.graphy.dev/api/v0/narrate \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"config": {
"type": "line",
"data": {
"columns": [
{
"key": "month",
"label": "Month"
},
{
"key": "users",
"label": "Active Users"
}
],
"rows": [
{
"month": "Jan",
"users": 1200
},
{
"month": "Feb",
"users": 1800
},
{
"month": "Mar",
"users": 3100
}
]
}
},
"userPrompt": "Write a punchy title and a one-line caption",
"metadata": {
"callId": "req-narrate-1",
"locale": "EN_US"
}
}
'import requests
url = "https://agents.graphy.dev/api/v0/narrate"
payload = {
"config": {
"type": "line",
"data": {
"columns": [
{
"key": "month",
"label": "Month"
},
{
"key": "users",
"label": "Active Users"
}
],
"rows": [
{
"month": "Jan",
"users": 1200
},
{
"month": "Feb",
"users": 1800
},
{
"month": "Mar",
"users": 3100
}
]
}
},
"userPrompt": "Write a punchy title and a one-line caption",
"metadata": {
"callId": "req-narrate-1",
"locale": "EN_US"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
config: {
type: 'line',
data: {
columns: [{key: 'month', label: 'Month'}, {key: 'users', label: 'Active Users'}],
rows: [
{month: 'Jan', users: 1200},
{month: 'Feb', users: 1800},
{month: 'Mar', users: 3100}
]
}
},
userPrompt: 'Write a punchy title and a one-line caption',
metadata: {callId: 'req-narrate-1', locale: 'EN_US'}
})
};
fetch('https://agents.graphy.dev/api/v0/narrate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://agents.graphy.dev/api/v0/narrate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'config' => [
'type' => 'line',
'data' => [
'columns' => [
[
'key' => 'month',
'label' => 'Month'
],
[
'key' => 'users',
'label' => 'Active Users'
]
],
'rows' => [
[
'month' => 'Jan',
'users' => 1200
],
[
'month' => 'Feb',
'users' => 1800
],
[
'month' => 'Mar',
'users' => 3100
]
]
]
],
'userPrompt' => 'Write a punchy title and a one-line caption',
'metadata' => [
'callId' => 'req-narrate-1',
'locale' => 'EN_US'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://agents.graphy.dev/api/v0/narrate"
payload := strings.NewReader("{\n \"config\": {\n \"type\": \"line\",\n \"data\": {\n \"columns\": [\n {\n \"key\": \"month\",\n \"label\": \"Month\"\n },\n {\n \"key\": \"users\",\n \"label\": \"Active Users\"\n }\n ],\n \"rows\": [\n {\n \"month\": \"Jan\",\n \"users\": 1200\n },\n {\n \"month\": \"Feb\",\n \"users\": 1800\n },\n {\n \"month\": \"Mar\",\n \"users\": 3100\n }\n ]\n }\n },\n \"userPrompt\": \"Write a punchy title and a one-line caption\",\n \"metadata\": {\n \"callId\": \"req-narrate-1\",\n \"locale\": \"EN_US\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://agents.graphy.dev/api/v0/narrate")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"config\": {\n \"type\": \"line\",\n \"data\": {\n \"columns\": [\n {\n \"key\": \"month\",\n \"label\": \"Month\"\n },\n {\n \"key\": \"users\",\n \"label\": \"Active Users\"\n }\n ],\n \"rows\": [\n {\n \"month\": \"Jan\",\n \"users\": 1200\n },\n {\n \"month\": \"Feb\",\n \"users\": 1800\n },\n {\n \"month\": \"Mar\",\n \"users\": 3100\n }\n ]\n }\n },\n \"userPrompt\": \"Write a punchy title and a one-line caption\",\n \"metadata\": {\n \"callId\": \"req-narrate-1\",\n \"locale\": \"EN_US\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://agents.graphy.dev/api/v0/narrate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"config\": {\n \"type\": \"line\",\n \"data\": {\n \"columns\": [\n {\n \"key\": \"month\",\n \"label\": \"Month\"\n },\n {\n \"key\": \"users\",\n \"label\": \"Active Users\"\n }\n ],\n \"rows\": [\n {\n \"month\": \"Jan\",\n \"users\": 1200\n },\n {\n \"month\": \"Feb\",\n \"users\": 1800\n },\n {\n \"month\": \"Mar\",\n \"users\": 3100\n }\n ]\n }\n },\n \"userPrompt\": \"Write a punchy title and a one-line caption\",\n \"metadata\": {\n \"callId\": \"req-narrate-1\",\n \"locale\": \"EN_US\"\n }\n}"
response = http.request(request)
puts response.read_body{
"message": "<string>"
}Generate a chart’s title, subtitle, and caption with natural language. Send a
Final response data:
Errors may also arrive as SSE events within a 200 response. See Error Codes.
GraphConfig and a prompt, receive an updated GraphConfig via Server-Sent Events. Narrative is written into config.content as TipTap JSON.
Request body
userPrompt is required — the narrative agent needs an instruction to write against. Which fields are produced (title only, title + caption, or all three) depends on the storytelling level derived from metadata.effort.
Response
The response is a Server-Sent Events stream. See SSE Format for parsing details.event: progress
data: {"message":"Generating narrative..."}
event: complete
data: {"config":{...},"response":{"message":"Wrote a title and caption"}}
interface NarrateResponse {
config: GraphConfig;
response: {
message: string;
};
}
HTTP Status Codes
| Status | Description |
|---|---|
| 200 | Success (stream begins) |
| 400 | Invalid request body |
| 401 | Invalid or missing API key |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
Authorizations
Your Graphy API key (starts with graphy_). Create one in the Graphy console at https://agents.graphy.dev/console/
Body
application/json
The graph configuration object used to render charts. See Graph Config Schema for the complete reference.
Show child attributes
Show child attributes
Natural language instruction describing the desired changes
Maximum string length:
10000Optional tracking information for requests
Show child attributes
Show child attributes
Response
SSE stream with progress and completion events
⌘I

