Examples
Worked Mangrove runs: an image, a document, a meeting recording, and an answer saved as a file.
Worked examples for specific ways people use the agents API.
Case 1: Sharing an image and getting Screen Time analysis
Someone exports their phone's Screen Time summary as a screenshot and wants more than the numbers read back to them — they want to know what to change. Uploading the image directly sends it to the vision model itself rather than running OCR on it, so bars, charts, and layout are read as visual content instead of as illegible text.
curl -X POST https://api.greenpt.ai/v1/agents/runs \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "question=Summarize my screen time this week and tell me what to cut back on" \
-F "file=@screen-time.png"{ "agentId": "9b6e2f31-4a7c-4e9d-8b21-5f0a3c8d6e12", "status": "queued" }Looking at the image is a forced step, not something the model decides to
do, so it runs as step 1 before the model ever sees the question. The
uploaded file is stored briefly and handed to the run as a temporary link,
which is what vision receives:
event: step_start
data: {"type":"step_start","step":1,"description":"Executing tool"}
event: tool_call
data: {"type":"tool_call","toolName":"vision","input":{"imageUrl":"https://s3.fr-par.scw.cloud/…/screen-time.png?X-Amz-Signature=…","question":"Summarize my screen time this week and tell me what to cut back on"}}
event: tool_result
data: {"type":"tool_result","toolName":"vision","result":"You spent 4h 12m on your phone today, up from a 3h 34m daily average this week. Social Networking is your biggest category at 1h 48m — mostly Instagram (58m) and TikTok (42m) — followed by Safari (31m) and Messages (24m). You picked up your phone 87 times."}
event: step_complete
data: {"type":"step_complete","step":1}
event: step_start
data: {"type":"step_start","step":2,"description":"LLM agentic loop"}
event: text
data: {"type":"text","text":"You spent 4h 12m on your phone today, against a 3h 34m daily average this week. Social Networking dominates at 1h 48m — Instagram (58m) and TikTok (42m). The change with the most impact: mute non-essential notifications for both and set a 45-minute daily limit on each."}
event: step_complete
data: {"type":"step_complete","step":2}
event: step_start
data: {"type":"step_start","step":3,"description":"Confirming the answer is complete before stopping"}
event: text
data: {"type":"text","text":"You spent 4h 12m on your phone today, against a 3h 34m daily average this week. …"}
event: step_complete
data: {"type":"step_complete","step":3}
event: done
data: {"type":"done","finalText":"You spent 4h 12m on your phone today, against a 3h 34m daily average this week. Social Networking dominates at 1h 48m — Instagram (58m) and TikTok (42m). The change with the most impact: mute non-essential notifications for both and set a 45-minute daily limit on each."}Step 3 is the final check, and its text is the same answer restated in
full — abbreviated above so the transcript stays readable. Use
done.finalText.
Same path as a documentUrl image
Passing documentUrl pointing at a hosted image takes the identical
forced-vision-step path — only the link's origin differs: the temporary
one from an upload here, your own host there. Vision is forced rather
than left for the model to call itself, unlike document_parse below,
because the run already knows the image is there. Making the model
restate the link as tool-call output would cost a round trip and risk a
mistyped character, for nothing.
Case 2: Summarizing a document by URL
Someone has a link to a report and wants the key points and numbers pulled
out, not the whole document read back to them. document_parse fetches the
URL directly from GreenPT's servers and converts it to markdown — the URL
must be publicly reachable; a link behind auth or on a private network fails
the fetch (see the callout below).
curl -X POST https://api.greenpt.ai/v1/agents/runs \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"question": "Summarize the key findings and pull out any concrete numbers",
"documentUrl": "https://files.example.com/whitepapers/eu-grid-carbon-intensity-2026.pdf"
}'{ "agentId": "5c8a1f42-6b3d-4e17-9a2c-8f1d3b7e40a6", "status": "queued" }Unlike the image case, document_parse is left for the model to call
itself rather than forced — a document URL is short regardless of the
file's size, so there's no cost in letting the model generate it as a
tool-call argument. Streaming (or polling) the run shows document_parse
running as a step the model chose to take, with text following once the
model has the markdown to answer from:
event: step_start
data: {"type":"step_start","step":1,"description":"LLM agentic loop"}
event: tool_call
data: {"type":"tool_call","toolName":"document_parse","input":"{\"url\":\"https://files.example.com/whitepapers/eu-grid-carbon-intensity-2026.pdf\"}"}
event: tool_result
data: {"type":"tool_result","toolName":"document_parse","result":"# EU Grid Carbon Intensity 2026\n\n## Key findings\n\nAverage grid carbon intensity across the EU fell to 158 gCO2e/kWh in 2026, down from 189 gCO2e/kWh in 2025..."}
event: text
data: {"type":"text","text":"The report finds EU grid carbon intensity averaged 158 gCO2e/kWh in 2026, down 16% from 189 gCO2e/kWh in 2025. Solar and wind combined reached 41% of generation, up from 34%. The steepest drop was in Q2, when intensity briefly fell below 90 gCO2e/kWh during a mid-day solar peak across Germany, France, and Spain."}
event: step_complete
data: {"type":"step_complete","step":1}
event: step_start
data: {"type":"step_start","step":2,"description":"Confirming the answer is complete before stopping"}
event: text
data: {"type":"text","text":"The report finds EU grid carbon intensity averaged 158 gCO2e/kWh in 2026, …"}
event: step_complete
data: {"type":"step_complete","step":2}
event: done
data: {"type":"done","finalText":"The report finds EU grid carbon intensity averaged 158 gCO2e/kWh in 2026, down 16% from 189 gCO2e/kWh in 2025. Solar and wind combined reached 41% of generation, up from 34%. The steepest drop was in Q2, when intensity briefly fell below 90 gCO2e/kWh during a mid-day solar peak across Germany, France, and Spain."}A failed fetch surfaces as a tool_result, not a run failure
If the URL can't be fetched (private, expired, wrong host),
document_parse returns an error string as its tool_result rather than
failing the run — the model sees Error: Failed to fetch document from URL: 404 Not Found and can act on it, e.g. asking for a working URL via
input_required instead of returning a confident-sounding non-answer.
Case 3: Summarizing a meeting
Someone has a recording of a meeting and wants a summary they can act on —
decisions, owners, and deadlines, not the transcript read back to them.
Passing audioUrl transcribes the recording before the model sees the
question, so the first thing the model does is answer with the whole meeting
already in context — it never has to decide to transcribe anything.
curl -X POST https://api.greenpt.ai/v1/agents/runs \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"question": "What were the decisions and action items?",
"audioUrl": "https://example.com/standup.mp3"
}'{ "agentId": "e274b8a1-3f56-4c9e-9d02-6a1f8b53c7de", "status": "queued" }A recording you hold locally is uploaded the same way any other file is, and takes the same path — no hosting needed:
curl -X POST https://api.greenpt.ai/v1/agents/runs \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "question=What were the decisions and action items?" \
-F "file=@standup.mp3"Transcription is a forced step, not something the model decides to do, so it
runs as step 1 before the model ever sees the question — the stream shows
audio_transcribe first, then the model's answer as step 2:
event: step_start
data: {"type":"step_start","step":1,"description":"Executing tool"}
event: tool_call
data: {"type":"tool_call","toolName":"audio_transcribe","input":{"url":"https://example.com/standup.mp3"}}
event: tool_result
data: {"type":"tool_result","toolName":"audio_transcribe","result":"Duration: 14:52\nDetected language: en\n\n[0:00] Speaker 0: Alright, let's get into it — where are we on the migration?\n[0:08] Speaker 1: Database side is done, we cut over Tuesday with no incidents. Still need to update the runbook.\n[0:22] Speaker 0: Can you own the runbook update by Friday?\n[0:25] Speaker 1: Yeah, Friday works.\n[0:31] Speaker 2: On the frontend side we're blocked on the design review — I need sign-off before I can start the rollout.\n[0:41] Speaker 0: I'll get you sign-off by tomorrow morning. Let's target rollout for next Wednesday."}
event: step_complete
data: {"type":"step_complete","step":1}
event: step_start
data: {"type":"step_start","step":2,"description":"LLM agentic loop"}
event: text
data: {"type":"text","text":"Decisions: target the frontend rollout for next Wednesday, pending design sign-off. Action items: Speaker 1 to update the migration runbook by Friday; Speaker 0 to deliver design sign-off to Speaker 2 by tomorrow morning. The database migration is complete with no incidents."}
event: step_complete
data: {"type":"step_complete","step":2}
event: step_start
data: {"type":"step_start","step":3,"description":"Confirming the answer is complete before stopping"}
event: text
data: {"type":"text","text":"Decisions: target the frontend rollout for next Wednesday, pending design sign-off. …"}
event: step_complete
data: {"type":"step_complete","step":3}
event: done
data: {"type":"done","finalText":"Decisions: target the frontend rollout for next Wednesday, pending design sign-off. Action items: Speaker 1 to update the migration runbook by Friday; Speaker 0 to deliver design sign-off to Speaker 2 by tomorrow morning. The database migration is complete with no incidents."}Speaker labels are numbers, not names
Diarization identifies distinct speakers but not who they are — the
transcript labels them Speaker 0, Speaker 1, and so on. The model maps
a label to a name only when the transcript or your question makes that
mapping clear (someone addressed by name, or named in the question
itself); otherwise the summary refers to them by number, as above.
A question that clearly needs a recording, asked without audioUrl and
without enough question text to be a pasted transcript, may end with an
awaiting_input status asking for one instead of guessing — see Handling
awaiting_input for what that looks like.
Case 4: Getting the answer back as a file
Someone wants the output as something they can pass on — a CSV to open in a spreadsheet, a markdown summary to drop into a document. Ask for it in the question, and Mangrove writes the file and answers with a link to it.
curl -X POST https://api.greenpt.ai/v1/agents/runs \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"question": "Compare the 2025 and 2026 grid intensity figures and save the comparison as a CSV I can download",
"documentUrl": "https://files.example.com/whitepapers/eu-grid-carbon-intensity-2026.pdf"
}'The model decides to call save_file itself, the same way it decides to
search the web. The link arrives as the tool's result, and Mangrove repeats
it in its answer:
event: tool_call
data: {"type":"tool_call","toolName":"save_file","input":"{\"filename\":\"eu-grid-intensity-2025-2026.csv\",\"content\":\"year,gco2e_per_kwh\\n2025,189\\n2026,158\\n\"}"}
event: tool_result
data: {"type":"tool_result","toolName":"save_file","result":"Saved \"eu-grid-intensity-2025-2026.csv\". Download link (valid until 2026-07-28T10:00:00.000Z): https://files.example.com/…?X-Amz-Signature=…"}The link carries its own signed authorization, so it needs no API key and can go straight to a browser or a colleague. That cuts both ways: anyone holding it can download the file until it expires, roughly a day after the run.
Saved files are text — markdown, CSV, JSON, plain text — up to 1 MB. Where
file export is not enabled, the call comes back as Error: File export is not enabled and Mangrove falls back to putting the content in its answer.
When Mangrove's answer does not link a file it saved, the run links it for
you: the answer, in result and in the done event's finalText, ends with
a Files list holding one markdown link per file. If you parse the answer
as one format, such as JSON, ask for the files to be named in it, or take the
links from the save_file results instead.