GreenPT Docs

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.

On this page