[[{“value”:”
Introduction
A chat agent only works when somebody opens the chat. Not everything needs to be chat-based though. A morning report or a system check is the same question every day, so instead of every user running it in the chat, run it once as a background job and mail the report to the whole team.
In the previous blog post I went through the anatomy of an agent and left one switch unexplained: Expose as an API-triggered job: https://community.sap.com/t5/technology-blog-posts-by-members/agentic-ai-on-btp-configuring-code-based-agents-on-the-fly/ba-p/14488352
In this blog post, I’ll show you what that switch does. Same definition, same toolsets, triggered by the SAP BTP Job Scheduling Service instead of by a human typing a question.
Problem
A chat is pull. Somebody has to open it, type the question and read the answer. For a question that is the same every morning, that is a lot of manual work spread over a lot of people.
Running it once as a background job gives you:
- No manual prompting. Nobody has to remember to ask.
- No duplicate runs. Two or three people asking the same thing get one report instead of three runs against the same system.
- Nothing to think about. Once it is scheduled it happens, awake or not.
Scenarios that fit this well:
- a daily system check: dumps, failed jobs, locks and whatever else is in your morning routine
- an analysis of the background job runs over the night or the weekend
- a transport overview of what moved and what is still open
- a check of the new SAP security notes against what is installed
One thing does not come for free: there is no user at 03:00. The chat forwards the signed-in user’s token, an unattended run has nobody to borrow an identity from. That is the part that needed solving.
Solution
Two entry points, one runtime. Next to the chat there is a scope-protected endpoint:
POST /api/agents/{slug}/run
POST /api/workflows/{slug}/run
Both answer 202 Accepted immediately and run the agent in a background task, which is what keeps them inside the 15 second budget. The scheduler is told the run started, not what it found.
Step 1: Expose the agent
Switch on Expose as an API-triggered job in the Job settings panel. This unlocks four fields:
- API slug:
monitoringbecomesPOST /api/agents/monitoring/run. - Run prompt: the message handed to the agent. The work is described by the instructions and skills, this only starts it. Empty means “Perform your configured check now and return the report.”
- Run timeout (seconds).
- Run as principal: the identity the run borrows. This is the field that matters.
For the ARC-1 toolset the auth mode is oauth2, so tokens are stored per user. At 03:00 there is no user, so the run is told whose stored token to use: a service account that has itself authorized this agent’s toolsets once, in a browser. The whole run acts as that account, delegated peers included, so give it read-only monitoring authorizations and no more.
Two preflight checks run before the agent starts, with deliberately different messages. No principal configured means nothing was ever authorized. A principal with no usable credential means something went stale and needs re-authorizing. Pointing someone at re-authorization when no service account exists is a dead end.
The Credential status panel underneath answers that at a glance, per toolset. There is also a nightly refresh of the tokens scheduled runs need, because between nightly runs nothing calls anything and a refresh token quietly expires.
Step 2: Bind the Job Scheduling Service
Only the scheduler may call that endpoint. A dedicated scope in xs-security.json does it:
{
"name": "$XSAPPNAME.JOBSCHEDULER",
"description": "Trigger scheduled agent and workflow runs (Job Scheduling Service)",
"grant-as-authority-to-apps": ["$XSSERVICENAME(agent-jobscheduler)"]
}
grant-as-authority-to-apps hands the scope to the jobscheduler instance itself. The scheduler calls the application directly and not through the approuter, so the route is deliberately absent from xs-app.json and this scope check is the whole protection.
Two ordering details, both of which cost me a deploy. XSUAA resolves $XSSERVICENAME(agent-jobscheduler) while it creates the instance, so declare the jobscheduler resource before uaa-service and set enable-parallel-deployments: false. Otherwise the deploy fails with “Could not find app for XSUAA instance with name ‘agent-jobscheduler’”.
The jobs themselves are not in mta.yaml. They live in the service instance, so a redeploy never rewrites a schedule somebody tuned by hand:
A job holds the action URL and the method, nothing else. The schedule is a separate object under it, with a cron pattern in the service’s own seven field format:
Every trigger arrives with the scheduler’s coordinates in the headers (x-sap-job-id, x-sap-job-schedule-id, x-sap-job-run-id), stored on the run so you can match a row here with a run log there.
The report
A chat answer is a conversation, a scheduled run needs a document. So the agent is called with a structured output type: a one sentence summary for the runs table and a body_md markdown document for the run page. Pydantic AI validates it as the output_type and retries the model on a mismatch, so the shape is enforced and not hoped for. Mermaid blocks are rendered, raw HTML is stripped.
What the run log says and what it doesn’t
The scheduler keeps its own history, worth reading carefully:
Execution at 04:23:41, completion at 04:54:08. The run does not take half an hour. That is the instance’s async execution timeout of 1800 seconds: the app answers 202 and never calls the Update Run Log callback, so the scheduler closes the entry itself when the timeout expires.
So the scheduler’s log tells you a trigger was delivered. Whether the agent found anything is what the runs list in the app is for. Posting that callback is still on my list.
Conclusion
Scheduling an agent is not really about cron, cron is ten minutes of work in the dashboard. The work is an identity for a run nobody is signed in to, an acknowledgement fast enough for 15 seconds, an output shaped for someone who was not in the conversation and enough care around overlap and shutdown that you can stop watching it.
Once that is in place, the step from “an agent I can ask” to “an agent that tells me” is one switch in a form.
Full source on GitHub: https://github.com/lemaiwo/btp-dynamic-multiagent-app
What’s Next?
The two jobs in the screenshots above point at a workflow rather than a single agent. In the next blog post, I’ll show you what a workflow is: multiple agents in a declared line, a fan-out step and per-item branches that join back together.
“}]]
Read More Technology Blog Posts by Members articles
#abap