When the AI generates a chart or queries tabular data, the API emits a visualization event carrying the complete structured payload — no separate fetch required. Your application receives everything needed to render the chart or table directly.
Visualizations are only available when using the API with a connected datasource. The visualization event is sent before the final message event.
Event structure
Every visualization event follows the same envelope:
event: visualization
data: {"id": "ab12xy3456", "type": "chart", "chart_type": "bar", ...}
{ "event" : "visualization" , "data" : { "id" : "ab12xy3456" , "type" : "chart" , "chart_type" : "bar" , ... }}
The data object contains all the information you need to render the visualization:
A unique 10-character alphanumeric identifier for this visualization. Use this to deduplicate events if needed.
The visualization type. One of "chart" or "table".
Charts
The AI produces charts when the user explicitly requests one. Seven chart types are supported, grouped into two categories:
Axis charts (bar, bar_horizontal, stacked_bar, line, area) — display data with X and Y axes. Data points use {value, label} format.
Segment charts (pie, donut) — display proportional parts of a whole. Data points use {value, text} format.
One of "bar", "bar_horizontal", "stacked_bar", "line", "area", "pie", or "donut".
Optional display title for the chart.
Container for the chart data. Array of data points. Minimum 1 item. See per-chart-type format below.
Label for the X axis. Required for axis charts (bar, bar_horizontal, stacked_bar, line, area). Omit for pie and donut.
Label for the Y axis. Required for axis charts (bar, bar_horizontal, stacked_bar, line, area). Omit for pie and donut.
Colors are not included in chart data — your application controls the color palette.
Bar chart
Use for comparing quantities across categories. Each item in values has:
Field Type Required Description valuenumberYes Numeric value for this data point labelstringNo Category label (max ~25 chars recommended)
{
"event" : "visualization" ,
"data" : {
"id" : "ab12xy3456" ,
"type" : "chart" ,
"chart_type" : "bar" ,
"title" : "Project Completion by Sector" ,
"data" : {
"values" : [
{ "label" : "Residential" , "value" : 85.5 },
{ "label" : "Commercial" , "value" : 62.3 },
{ "label" : "Infrastructure" , "value" : 91.0 },
{ "label" : "Mixed Use" , "value" : 74.8 }
],
"x_axis_label" : "Sector" ,
"y_axis_label" : "Completion %"
}
}
}
Horizontal bar chart
Use for ranking items with long labels — project names, contractor names, risk descriptions. Horizontal orientation prevents label overlap. Each item in values has:
Field Type Required Description valuenumberYes Numeric value for this data point labelstringNo Category label
{
"event" : "visualization" ,
"data" : {
"id" : "ef56gh7890" ,
"type" : "chart" ,
"chart_type" : "bar_horizontal" ,
"title" : "Top 5 Projects by Budget (AED millions)" ,
"data" : {
"values" : [
{ "label" : "Saadiyat Lagoons Phase 2" , "value" : 245.0 },
{ "label" : "The Grove Louvre Residence" , "value" : 198.5 },
{ "label" : "Al Raha Beach Tower" , "value" : 172.3 },
{ "label" : "Yas Bay Waterfront" , "value" : 156.8 },
{ "label" : "Mamsha Al Saadiyat" , "value" : 134.2 }
],
"x_axis_label" : "Project" ,
"y_axis_label" : "Budget (AED M)"
}
}
}
Stacked bar chart
Use for showing composition within categories — breakdown by severity, allocation by phase. The data shape is identical to the standard bar chart. Each item in values represents a stacked segment.
Field Type Required Description valuenumberYes Numeric value for this data point labelstringNo Category label
{
"event" : "visualization" ,
"data" : {
"id" : "ij78kl9012" ,
"type" : "chart" ,
"chart_type" : "stacked_bar" ,
"title" : "Risk Count by Severity" ,
"data" : {
"values" : [
{ "label" : "High Impact" , "value" : 12 },
{ "label" : "Medium Impact" , "value" : 28 },
{ "label" : "Low Impact" , "value" : 45 },
{ "label" : "Closed" , "value" : 18 }
],
"x_axis_label" : "Severity" ,
"y_axis_label" : "Count"
}
}
}
Line chart
Use for time-series or sequential data. Each item in values has:
Field Type Required Description valuenumberYes Numeric value for this data point labelstringNo X-axis label (e.g. month, date)
{
"event" : "visualization" ,
"data" : {
"id" : "cd34ef5678" ,
"type" : "chart" ,
"chart_type" : "line" ,
"title" : "Monthly Contract Value (AED)" ,
"data" : {
"values" : [
{ "label" : "Jan" , "value" : 12500000 },
{ "label" : "Feb" , "value" : 14200000 },
{ "label" : "Mar" , "value" : 11800000 },
{ "label" : "Apr" , "value" : 16400000 },
{ "label" : "May" , "value" : 18900000 }
],
"x_axis_label" : "Month" ,
"y_axis_label" : "Contract Value (AED)"
}
}
}
Area chart
Use for cumulative volume or magnitude over time. Visually similar to a line chart with a filled area beneath the line. Each item in values has:
Field Type Required Description valuenumberYes Numeric value for this data point labelstringNo X-axis label (e.g. quarter, date)
{
"event" : "visualization" ,
"data" : {
"id" : "mn90op1234" ,
"type" : "chart" ,
"chart_type" : "area" ,
"title" : "Cumulative Units Sold — Saadiyat Lagoons" ,
"data" : {
"values" : [
{ "label" : "Q1 2025" , "value" : 48 },
{ "label" : "Q2 2025" , "value" : 112 },
{ "label" : "Q3 2025" , "value" : 189 },
{ "label" : "Q4 2025" , "value" : 267 },
{ "label" : "Q1 2026" , "value" : 310 }
],
"x_axis_label" : "Quarter" ,
"y_axis_label" : "Units Sold"
}
}
}
Pie chart
Use for composition of a whole (7 or fewer segments recommended). Each item in values has:
Field Type Required Description valuenumberYes Segment size (numeric, not necessarily a percentage) textstringYes Display label for this segment (used for the legend)
{
"event" : "visualization" ,
"data" : {
"id" : "gh56ij7890" ,
"type" : "chart" ,
"chart_type" : "pie" ,
"title" : "Budget Allocation by Category" ,
"data" : {
"values" : [
{ "text" : "Construction" , "value" : 45 },
{ "text" : "Design" , "value" : 30 },
{ "text" : "Permits" , "value" : 15 },
{ "text" : "Other" , "value" : 10 }
]
}
}
}
Donut chart
Same structure as pie, with "chart_type": "donut".
{
"event" : "visualization" ,
"data" : {
"id" : "kl78mn9012" ,
"type" : "chart" ,
"chart_type" : "donut" ,
"title" : "Project Status Distribution" ,
"data" : {
"values" : [
{ "text" : "On Track" , "value" : 35 },
{ "text" : "At Risk" , "value" : 12 },
{ "text" : "Delayed" , "value" : 7 },
{ "text" : "Complete" , "value" : 18 }
]
}
}
}
Tables
When the AI executes a SQL query that returns multiple rows, it automatically emits the results as a visualization event with type: "table".
A table visualization is only emitted for queries returning 2 or more rows . Single-row results are described in the text message instead.
Column header names, derived from SQL column names and formatted as title case (e.g. project_name becomes "Project Name").
A 2D array of pre-formatted string values. Every cell is a string — numbers include comma separators, floats are rounded to 2 decimal places, and null values become empty strings.
{
"event" : "visualization" ,
"data" : {
"id" : "op90qr1234" ,
"type" : "table" ,
"headers" : [
"Project Name" ,
"Status" ,
"Budget" ,
"Completion %"
],
"rows" : [
[ "Al Raha Beach Tower" , "Construction" , "12,800,000" , "73.46" ],
[ "Yas Mall Extension" , "Design" , "8,400,000" , "15.00" ],
[ "Admin Building Retrofit" , "DLP and Project Closeout" , "5,200,000" , "100.00" ],
[ "Marina Gate Phase 2" , "Contractor Procurement" , "22,100,000" , "8.50" ],
[ "Green Spine Boulevard" , "Construction" , "31,600,000" , "44.20" ]
]
}
}
Source value Formatted string 5200000 (integer)"5,200,000"73.456 (float)"73.46"None / null"" (empty string)True / False (boolean)"True" / "False""text" (string)"text" (unchanged)
Handling visualizations in your application
Use the type field to branch between charts and tables, then chart_type to select the appropriate chart renderer.
interface VisualizationData {
id : string ;
type : "chart" | "table" ;
chart_type ?: "bar" | "bar_horizontal" | "stacked_bar" | "line" | "area" | "pie" | "donut" ;
title ?: string ;
data ?: {
values : Array <{ label ?: string ; text ?: string ; value : number }>;
x_axis_label ?: string ;
y_axis_label ?: string ;
};
headers ?: string [];
rows ?: string [][];
}
function handleVisualization ( viz : VisualizationData ) {
if ( viz . type === "table" ) {
renderTable ( viz . headers ! , viz . rows ! );
return ;
}
const axisOptions = {
xLabel: viz . data ! . x_axis_label ,
yLabel: viz . data ! . y_axis_label ,
};
switch ( viz . chart_type ) {
case "bar" :
renderBarChart ( viz . title , viz . data ! . values , axisOptions );
break ;
case "bar_horizontal" :
renderHorizontalBarChart ( viz . title , viz . data ! . values , axisOptions );
break ;
case "stacked_bar" :
renderStackedBarChart ( viz . title , viz . data ! . values , axisOptions );
break ;
case "line" :
renderLineChart ( viz . title , viz . data ! . values , axisOptions );
break ;
case "area" :
renderAreaChart ( viz . title , viz . data ! . values , axisOptions );
break ;
case "pie" :
case "donut" :
renderPieChart ( viz . title , viz . data ! . values , viz . chart_type );
break ;
}
}
// Wire into your SSE event handler
async function streamChat ( message : string , integrationId : string , token : string ) {
const response = await fetch ( "https://api.trellis.sh/v1/chats" , {
method: "POST" ,
headers: {
Authorization: `Bearer ${ token } ` ,
"Content-Type" : "application/json" ,
Accept: "text/event-stream" ,
},
body: JSON . stringify ({ message , integration_id: integrationId }),
});
const reader = response . body ! . getReader ();
const decoder = new TextDecoder ();
let buffer = "" ;
let currentEvent = "" ;
while ( true ) {
const { done , value } = await reader . read ();
if ( done ) break ;
buffer += decoder . decode ( value , { stream: true });
const lines = buffer . split ( " \n " );
buffer = lines . pop () || "" ;
for ( const line of lines ) {
if ( line . startsWith ( "event: " )) {
currentEvent = line . slice ( 7 );
} else if ( line . startsWith ( "data: " ) && currentEvent ) {
const data = JSON . parse ( line . slice ( 6 ));
if ( currentEvent === "visualization" ) {
handleVisualization ( data );
} else if ( currentEvent === "message" ) {
displayMessage ( data . content );
}
currentEvent = "" ;
}
}
}
}
import requests
import json
def handle_visualization ( viz : dict ):
"""Dispatch visualization data to the appropriate renderer."""
if viz[ "type" ] == "table" :
render_table(viz[ "headers" ], viz[ "rows" ])
return
chart_type = viz.get( "chart_type" )
title = viz.get( "title" )
values = viz[ "data" ][ "values" ]
axis_kwargs = dict (
x_label = viz[ "data" ].get( "x_axis_label" ),
y_label = viz[ "data" ].get( "y_axis_label" ),
)
if chart_type == "bar" :
render_bar_chart(title, values, ** axis_kwargs)
elif chart_type == "bar_horizontal" :
render_horizontal_bar_chart(title, values, ** axis_kwargs)
elif chart_type == "stacked_bar" :
render_stacked_bar_chart(title, values, ** axis_kwargs)
elif chart_type == "line" :
render_line_chart(title, values, ** axis_kwargs)
elif chart_type == "area" :
render_area_chart(title, values, ** axis_kwargs)
elif chart_type in ( "pie" , "donut" ):
render_pie_chart(title, values, hole = (chart_type == "donut" ))
def stream_chat ( message : str , integration_id : str , token : str ):
"""Stream a chat message and handle visualization events."""
response = requests.post(
"https://api.trellis.sh/v1/chats" ,
headers = {
"Authorization" : f "Bearer { token } " ,
"Content-Type" : "application/json" ,
"Accept" : "text/event-stream" ,
},
json = { "message" : message, "integration_id" : integration_id},
stream = True ,
)
current_event = None
for line in response.iter_lines( decode_unicode = True ):
if not line:
continue
if line.startswith( "event: " ):
current_event = line[ 7 :]
elif line.startswith( "data: " ) and current_event:
data = json.loads(line[ 6 :])
if current_event == "visualization" :
handle_visualization(data)
elif current_event == "message" :
print (data[ "content" ])
current_event = None