script_run error script_tool_forbidden: discovery calls belong outside
script_tool_forbidden means a Sume script called a discovery tool (tools_list, tools_schema, mcp_health, search_tools) or script_run. Call them from the turn.

script_tool_forbidden is one of four stop codes from the Sume script_run tool. It means the script called a discovery tool (search_tools, get_tool_details, call_tool, tools_list, tools_schema or mcp_health) or script_run itself. Make those calls from the turn, not from a script.
What the server tells the agent
The tool's next steps say the same thing in one line: call discovery tools from the turn. The error is deterministic. The same script will fail the same way until the call is removed, so retrying it is wasted work.
| Code | Meaning | Fix |
|---|---|---|
| script_tool_forbidden | script called a discovery tool or script_run | move the call to the turn |
| script_timeout | the script ran past its time limit | split the work |
| script_call_budget_exceeded | more tool calls than max_calls | split the batch or raise max_calls within its ceiling |
| script_paid_budget_exceeded | more paid calls than max_paid_calls | same, for paid calls |
The right shape
Discover first, then script. In the turn, call tools_list and, for the one tool you will loop, tools_schema. Then write a script that only calls the loop tool and returns job ids. The progressive discovery post describes the two-step read. Inside the script each paid create still needs its own idempotency_key, since the gates are the same ones the turn would apply.
Pre-flight lint
A cheap check in your harness catches the mistake before a call is spent. The list below is the one from the tool's guidance.
FORBIDDEN = {
'search_tools', 'get_tool_details', 'call_tool',
'tools_list', 'tools_schema', 'mcp_health', 'script_run',
}
def lint(script_source: str) -> list[str]:
return sorted(n for n in FORBIDDEN if n in script_source)
print(lint('await tools.tts_create({})'))
print(lint('await tools.tools_list({})'))Limits
A substring check is crude and can flag a name that appears only in a comment. Treat it as a hint. The server remains the source of truth, and it is the server that returns the stop code.
Why discovery is excluded
A script is meant to be a fixed loop over known tools: the same calls with the same gates, run on the Sume side, with only the returned value coming back. Discovery and health tools exist to decide what to call, which is a decision for the turn. Letting a script discover tools would also let a script call the script runner again.
Checklist before you ship
- Run tools_list, tools_schema and mcp_health from the turn, before the script.
- Pass the discovered facts into the script as constants.
- Never call script_run from inside a script.
- Treat script_tool_forbidden as a script bug, not as a retryable error.
Sources
Related posts
More in Developers
- script_text, words, cues or segments: which caption input to send
Sume captions take only one of script_text, words, cues, segments. Your pick decides whether speech-to-text runs and what happens on a silent clip.
- SDK waitForJob reads per minute: the 2-second floor, and 10 jobs
The Sume SDK polls a job at least every 2 seconds, and a longer next_poll_after_seconds wins. That is up to 30 reads a minute per job. A webhook removes them.
- See every webhook attempt for a Sume video job: webhook.delivery
Did Sume reach your endpoint? Read webhook_delivery on the job and the webhook.delivery events: statuses pending to exhausted, last_error and attempt count.
- Seedance 2.5 API 'coming soon' on ModelArk: what to call today
ByteDance says Seedance 2.5 API access is coming via ModelArk. Sume lists seedance-2.5 now: id, limits, price, and how to keep a later switch cheap.
Written by Sume