You are here: Building with agents › Deciding what to build › Shape 3
Summary#
At the translation bureau every commission now goes through the same steps: it is prepared, then it is ready for a translator. Preparing is the agent's job. If the agent cannot finish — the model is down, the money ran out, somebody stopped it — the commission does not get stuck: the process sends it to the manager instead.
This is the third shape: agents inside a business process. The process is in charge. The agent does one step of it.

One run of a supplier-qualification process, from a tool maker's app: where it stands, each state it was in, and what each agent did there.
Description#
What does it add?#
| You add | It gives you |
|---|---|
| a [[workflow-index|workflow]] | the process itself: its states, and what moves it from one to the next |
| [[agent-kit-processes|Osysharp.Processes]] | every run listed, drawn and told in order, for somebody who was not there |
| [[agent-kit-agents-processes|Osysharp.Agents.Processes]] | nothing to write: it arrives by itself, tells each agent step on the run, and charges the step to the run |
| [[agent-kit-agents-inspector|Osysharp.Agents.Inspector]] | one piece of an agent's work on one page, opened from the run |
What do you decide now?#
- Who a step works for. In a process nobody is signed in. Said nothing, the agent works with its own roles
alone.
Agents.Of<X>().For(person)makes a step work for a person — the buyer who asked, say — and then it never does more than that person may. See [[agent-processes#for]]. - What the process does when a step does not finish. The call answers an outcome, not an exception:
Done, or not. Your workflow branches on it like on anything else. See [[agent-processes#failed]]. - What each run may spend.
SetProcessBudget("CommissionFlow", 2.00m)holds every run of that workflow to two dollars: its agent steps and its own model calls together. See [[agent-processes#cost]]. - Who follows processes. The policy
FollowsProcessessays who may open the list of runs and a run. Each thing on a run's page is then shown under the rule of the record it is read from: the money to whoever reads model spend, an agent's work to whoever may read that work. See [[workflow-processes-kit#who]]. - Who an approval inside a step goes to. As in shape 2. When the step works for nobody, a proposal goes to the agent's [[agent-guide-glossary#sponsor|sponsor]]. The process waits, for days if it takes days.
What do you get without writing it?#
- A step that waits on a person makes the whole process wait, and it goes on from the same point.
- Stopping the process calls off the work it was waiting for. See [[agent-processes#stopped]].
- Every agent step is a line on the run: given, waiting on a person, finished, failed, stopped.
- The receipt of each piece of work says which run, which workflow and which state gave it.
- What the run cost is what its agent steps cost. Nothing is counted twice.
What does the whole app look like?#
What is new since shape 1 is marked NEW. Approvals work here exactly as in
shape 2; they are left out so the page stays short.
app Bureau {
use Osysharp.Agents;
use Osysharp.Agents.Testing;
use Osysharp.Agents.Inspector; // NEW
use Osysharp.Agents.Processes; // NEW: the bridge
use Osysharp.Processes; // NEW
use Osysharp.Llm;
use Osysharp.Identity;
use Osysharp.Observability;
use Osysharp.Workflow;
use Osysharp.Diagrams;
use Osysharp.Ui;
model "model/**/*.osy";
}
using Osysharp.Identity;
using Osysharp.Llm;
using Osysharp.Workflow;
using Osysharp.Agents;
using Osysharp.Agents.Testing;
using Osysharp.Agents.Inspector;
using Osysharp.Processes;
using Osysharp.Agents.Processes;
app.Llm = new LlmSetup {
Models = [ new LlmModel { Name = "Everyday", Client = new ScriptedModel { Name = "Everyday" }, Model = "scripted-everyday",
Price = new LlmPrice { Input = 1, Output = 5 }, MaxOutputTokens = 1000 } ],
Default = "Everyday",
};
app.Agents = new AgentsSetup { Agents = [new Preparer()] };
app.Processes = new ProcessesSetup { Processes = [new CommissionHandling()] }; // NEW
[Role] enum BureauRole { Authenticator, Translator, Manager, Assistant }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsTranslator => RoleGrant.Any(g => g.Member == user && g.Role == BureauRole.Translator);
policy IsManager => RoleGrant.Any(g => g.Member == user && g.Role == BureauRole.Manager);
policy IsAssistant => RoleGrant.Any(g => g.Member == user && g.Role == BureauRole.Assistant);
policy UsesAgents => IsTranslator || IsManager;
policy SupervisesAgents => IsManager;
policy FollowsProcesses => IsTranslator || IsManager; // NEW — decision 4
policy ReadsModelUsage => IsManager;
policy ManagesModelSpend => IsManager;
entity Person : Principal {
security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}
// NEW: where a commission stands.
enum CommissionStage { Preparing, Ready, [Label("With the manager")] WithTheManager }
entity Commission {
[Required, MaxLength(200)] string Title;
[MaxLength(40)] string Into = "";
[MaxLength(400)] string Note = "";
CommissionStage Stage = CommissionStage.Preparing;
security { allow read, create, update when IsTranslator || IsManager || IsAssistant; }
}
public class Preparation {
public int Terms;
public string Summary = "";
}
[Action("Look up the house terms for a language", Query)]
public List<string> HouseTerms(string language) {
return language == "Finnish" ? ["sopimus: agreement", "toimitus: delivery"] : [];
}
public class PrepareCommission : Skill<Commission, Preparation> {
public override string Description => "Prepare one commission for its translator.";
public override List<ActionInfo> Tools => [Actions.Of(HouseTerms)];
public override string Brief(Commission c) => $"{c.Title}, into {c.Into}. Its id is {c.Id}.";
}
[Actor(Roles = [BureauRole.Assistant])]
public class Preparer : Agent {
public static Person Principal => new Person { Email = "[email protected]", DisplayName = "Preparer" };
public override string Purpose => "You prepare commissions for the translators of a small bureau.";
public override List<SkillBase> Skills => [new PrepareCommission()];
}
/// NEW — the process. Its agent step is an ordinary call: typed in, typed out, and the outcome decides.
workflow CommissionFlow {
Tracks = Commission.Stage; Autostart = false; Initial = Preparing;
state Preparing {
enter {
var prepared = await Agents.Of<Preparer>().Do(new PrepareCommission(), this.Item, about: "Preparing " + this.Item.Title);
if (!prepared.Done) {
// Decision 2: the step did not finish. The process does not get stuck: a person takes it.
Processes.Problem("The preparer could not finish, so the manager prepares this one.");
goto WithTheManager;
}
this.Item.Note = prepared.Result.Summary;
goto Ready;
}
}
terminal success Ready { }
terminal cancel WithTheManager { Message = "handed to the manager"; }
}
/// NEW — says which workflow is a business process, so its runs are listed and told.
public class CommissionHandling : Process<Commission> {
public override string Name => "Commission handling";
public override string Workflow => "CommissionFlow";
public override WorkflowGraph Graph => CommissionFlow.Graph;
public override string TitleOf(Commission c) => c.Title;
public override List<WorkflowAuditEntry> HistoryOf(Commission c) => CommissionFlow.For(c).Audit;
}
Commission TakeIn(string title) {
var commission = new Commission { Title = title, Into = "Finnish" };
UnitOfWork.Commit();
Workflow.Run(commission);
return commission;
}
[TestFixture]
void Office() {
var otto = new Person { Email = "[email protected]", DisplayName = "Otto Reyes" };
new RoleGrant { Member = otto, Role = BureauRole.Manager };
new AgentSeat { Agent = "Preparer", Sponsor = otto };
}
principal Otto => Person.Single(p => p.Email == "[email protected]");
[Test(Office)]
void The_agents_step_moves_the_process_on_and_is_told_on_its_run() {
// The preparer, scripted: it looks the house terms up and hands back what it found.
ScriptedAnswer.Stub((model, request) => {
if (request.Messages.Count(m => m.Role == "assistant") == 0) { return Says.Tool(Actions.Of(HouseTerms), "{\"language\":\"Finnish\"}", 300, 20); }
return Says.HandBack(new Preparation { Terms = 2, Summary = "Two house terms apply." }, 400, 40);
});
runas (Otto) {
var commission = TakeIn("Supply agreement");
Workflow.Settle(commission);
Assert.Equal(CommissionStage.Ready, Commission.Single().Stage); // the typed result decided
Assert.Equal("Two house terms apply.", Commission.Single().Note);
// The receipt says which run of which workflow gave it the work, and in which state.
var step = AgentTask.Single();
Assert.Equal("CommissionFlow", step.RunWorkflow);
Assert.Equal("Preparing", step.RunState);
// The run tells the step, in the state that gave it — and costs what the step cost.
var run = ProcessRunOf(ProcessRunAbout("CommissionFlow", commission.Id)!.Id)!;
Assert.Equal("Supply agreement", run.Card.Title);
Assert.Equal(ProcessStatus.Finished, run.Card.Status);
Assert.Equal(["Preparer was given work: prepare commission.", "Preparer finished: prepare commission."], run.Stays[0].Moments.Select(m => m.Said).ToList());
Assert.Equal(step.SpentMicros / 1000000m, run.Cost.Spent);
}
}
[Test(Office)]
void A_limit_for_each_run_stops_the_step_and_a_person_takes_over() {
// The preparer, scripted: it looks the house terms up and hands back what it found.
ScriptedAnswer.Stub((model, request) => {
if (request.Messages.Count(m => m.Role == "assistant") == 0) { return Says.Tool(Actions.Of(HouseTerms), "{\"language\":\"Finnish\"}", 300, 20); }
return Says.HandBack(new Preparation { Terms = 2, Summary = "Two house terms apply." }, 400, 40);
});
runas (Otto) {
// Decision 3: each run may spend a tenth of a cent. The preparer's first call would hold more: nothing is sent.
SetProcessBudget("CommissionFlow", 0.001m);
UnitOfWork.Commit();
var commission = TakeIn("Supply agreement");
Workflow.Settle(commission);
Assert.Equal(CommissionStage.WithTheManager, Commission.Single().Stage);
Assert.Equal(AgentTaskStatus.Stopped, AgentTask.Single().Status);
var run = ProcessRunOf(ProcessRunAbout("CommissionFlow", commission.Id)!.Id)!;
Assert.Equal("The preparer could not finish, so the manager prepares this one.", run.Stays[0].Moments.Last().Said);
}
}What would make this the wrong shape?#
- There are no fixed steps: somebody owns an outcome and tries things until it holds. That is an [[agent-guide-glossary#assignment|assignment]], not a process. See [[agent-guide-choosing#assignment-or-process]].
- There is only one step. That is shape 1 or shape 2.
What to read next#
- Shape 4 — a team of agents with a coordinator — the last shape: one agent in charge hands pieces to specialists
- A supplier is qualified — five days, four agents, one that failed, and a three-day wait — a real process of this shape, told day by day
- One piece of work, followed end to end — one piece of work followed through every kit
Go deeper#
- Agents in business processes — an agent's work as a step of a workflow, told on the run (Agents.Processes kit) — an agent's work as a step of a workflow
- Processes — every run of a business process, listed and read from start to now (Processes kit) — the Processes kit: runs, the picture, the telling
- The inspector — one piece of an agent's work, readable by a person who was not there (Agents.Inspector kit) — one piece of work on one page
- Workflows (the run that outlives the request) — workflows: states, events, waiting
- [[agent-testing#process]] — testing a whole process without a model