Core concepts / Execution
Execution
Once you create a mandate, Shadway runs it. It plans the next step, takes an action, waits when it has to, and recovers after a crash, all on its own, until the job ends. Your app watches. It doesn't drive the work.
How every action runs
The model never runs an action itself. It suggests one, and Shadway does the rest:
- The model proposes an action, like
email.send. - Shadway checks it against the mandate's limits.
- If it's allowed, an integration runs it with its own credentials.
- Shadway records what was decided and what happened.
Every action goes through all four steps, every time.
mailbox.replyReply to the supplier with a counter-offer- ProposeThe model suggests a typed action
- CheckInside every limit
- ExecuteSent through the connected mailbox
- RecordAction and result recorded
The integration executes with its own credentials. The model never sees them.
Two things come out of this. The model never sees a credential: it asks for
email.send, and the integration holds the keys. And a blocked action is recorded
as blocked, never as done, so the record always shows the difference between what
the agent tried and what actually happened.
How work survives a crash
Shadway saves every step as it goes. If the server restarts, redeploys, or crashes in the middle of a job, nothing is lost: the mandate picks up at the exact step where it stopped, with everything it knew.
This is also why waiting is free. A waiting mandate isn't a running process. It's a saved spot that a reply, a timer, or a decision starts back up.
Whether an action is safe to repeat is part of each capability's definition. Its
idempotency setting says the action is safe to repeat on its own, safe to repeat
when the vendor honors an idempotency key, not safe to repeat but checkable
afterward (reconcilable), or none of these. In the last case, Shadway won't
repeat it at all. If Shadway can't confirm what happened, it leaves the result
unknown. It never quietly turns that into a success or a reason to try again.
Request idempotency
Every API call that changes something carries an Idempotency-Key header. Both
SDKs add one automatically and reuse it when they retry, so a dropped connection
never creates two mandates. Set your own key when a retry might happen across app
restarts:
const mandate = await shadway.mandates.create(
{
agent: agent.id,
objective: "Renew the domain registration before it lapses.",
},
{ idempotencyKey: "renewal-2026-q3" },
);mandate, err := client.Mandates.Create(ctx, shadway.MandateCreateParams{
Agent: agent.ID,
Objective: "Renew the domain registration before it lapses.",
}, shadway.WithIdempotencyKey("renewal-2026-q3"))
if err != nil {
log.Fatal(err)
}
fmt.Println(mandate.ID) // The same ID on every retry of this creation.The same key with the same request returns the first result. The same key with a different request is rejected, so a retry can't quietly do something new. Keys last at least 24 hours.
Watch execution through events
Shadway reports each action as an event: capability.execution_started,
capability.execution_completed, and capability.execution_failed, each with the
capability name, how long it took, and any error.
import { isKnownEvent } from "shadway";
for await (const event of shadway.mandate(mandate.id).events.stream({ signal })) {
if (!isKnownEvent(event)) continue;
if (event.type === "capability.execution_completed") {
const execution = event.data.execution;
console.log(execution.capability, execution.durationMs ?? "?", "ms");
}
if (event.type === "capability.execution_failed") {
console.log("failed:", event.data.execution.error?.message);
}
}for event, err := range client.Mandate(mandate.ID).Stream(ctx, shadway.EventStreamOptions{}) {
if err != nil {
log.Fatal(err)
}
data, ok := event.Data.(*shadway.CapabilityExecutionEventData)
if !ok {
continue
}
switch event.Type {
case shadway.EventTypeCapabilityExecutionCompleted:
if ms := data.Execution.DurationMs; ms != nil {
fmt.Println(data.Execution.Capability, *ms, "ms")
}
case shadway.EventTypeCapabilityExecutionFailed:
if data.Execution.Error != nil {
fmt.Println("failed:", data.Execution.Error.Message)
}
}
}For the full streaming setup, including resuming after a restart, skipping duplicates, and filling gaps, see Stream a mandate's events.
Changing limits on a running mandate
Shadway re-reads the mandate before every step, so changes take effect on a
running mandate without stopping it. An
amendment applies at
the next step. A cancellation takes effect at the next safe point. If expiresAt
passes, the next action won't run, no matter how long the mandate has been going.