Core concepts / Approvals
Approvals
Sometimes a mandate reaches an action that needs a person to sign off before it can go ahead. That's an approval. The mandate waits until someone approves or rejects it.
Shadway captures the exact action it's about to take, a person reviews it, and the decision goes into the mandate's record either way.
Where approvals come from
You set up some approvals yourself. Others happen automatically:
authority.approval.requiredFor: actions that always need sign-off in this mandate, no matter the amount.authority.spend.approvalThreshold: any single action above this amount needs sign-off, even if it's under the total spending limit.- Automatic: Shadway pauses an action on its own when it's risky and hard to undo, or when a costly action is based on outside data it hasn't confirmed.
Treat every pending approval as real, even ones you didn't set up. An approval you didn't ask for means Shadway paused the action on purpose, not that something went wrong.
What a person reviews
procurement.create_poCommits $7,900 with a supplier for 200 units of part #48-221, delivered Friday.
Arguments are shown redacted. Secret values never appear here.The approval includes the question, the exact action it's about to take, and when the request expires. The action's arguments are shown with secrets hidden. Secret values never show up in an approval, in events, or in logs.
Present an approval and record the decision
Wait for an approval on a mandate you're running, or list all the pending approvals in your workspace to build a review inbox.
import { ShadwayTimeoutError } from "shadway";
const work = shadway.mandate(mandateId);
try {
const approval = await work.waitForApproval({ timeout: "30m" });
if (approval) {
// Render these fields in your application's approval screen.
console.log({
id: approval.id,
question: approval.question,
action: approval.action,
expiresAt: approval.expiresAt,
});
} else {
console.log("The mandate ended without asking for an approval.");
}
} catch (error) {
if (!(error instanceof ShadwayTimeoutError)) throw error;
console.log("No approval yet. The mandate continues on the server.");
}work := client.Mandate(mandateID)
approval, err := work.WaitForApproval(ctx, shadway.WaitOptions{
Timeout: 30 * time.Minute,
})
if err != nil {
if shadway.IsTimeout(err) {
fmt.Println("No approval yet. The mandate continues on the server.")
return
}
log.Fatal(err)
}
if approval == nil {
fmt.Println("The mandate ended without asking for an approval.")
return
}
// Render these fields in your application's approval screen.
fmt.Println(approval.ID, approval.Question)
fmt.Println(approval.Action.Capability, approval.Action.Consequence)Only call the deciding function after a person with the right access has reviewed the action and made a decision. Sign that person in and check that they're allowed to act on this mandate before you call it.
async function decide(
approvalId: string,
mandateId: string,
decision: "approve" | "reject",
reason: string,
) {
const approval = await shadway.approvals.get(approvalId);
if (approval.mandate !== mandateId || approval.status !== "pending") {
throw new Error("This approval is not pending for this mandate");
}
if (decision === "approve") {
return shadway.approvals.approve(approval.id, { reason });
}
return shadway.approvals.reject(approval.id, { reason });
}func decide(ctx context.Context, client *shadway.Client, approvalID, mandateID, decision, reason string) (*shadway.Approval, error) {
approval, err := client.Approvals.Get(ctx, approvalID)
if err != nil {
return nil, err
}
if approval.Mandate != mandateID || approval.Status != shadway.ApprovalStatusPending {
return nil, errors.New("this approval is not pending for this mandate")
}
if decision == "approve" {
return client.Approvals.Approve(ctx, approval.ID, &shadway.ApprovalApproveParams{Reason: &reason})
}
return client.Approvals.Reject(ctx, approval.ID, &shadway.ApprovalRejectParams{Reason: &reason})
}Decisions are final, and approvals expire
The first decision wins. Sending the same decision again returns the original result, so a retry after a dropped connection is safe. Sending the opposite decision is rejected, so it can't overwrite the first one.
Every approval expires, at most seven days after it's requested. A decision that
lands after it expires marks the approval expired instead of running the action.
A late approval never runs. Rejecting refuses one action, not the whole job. Cancel
the mandate if you want everything to stop. Canceling a mandate also clears its
pending approvals.
Pass sensitive values with a decision
Some approved actions need a value only a person has, like a one-time code or a
password for an old system. Pass it in inputs when you approve:
await shadway.approvals.approve(approvalId, {
reason: "Verified with the account holder.",
inputs: { otp: "483921" },
});reason := "Verified with the account holder."
_, err := client.Approvals.Approve(ctx, approvalID, &shadway.ApprovalApproveParams{
Reason: &reason,
Inputs: map[string]string{"otp": "483921"},
})
if err != nil {
log.Fatal(err)
}These inputs are only for running this one action. Shadway keeps them briefly as encrypted data, never shows them to the model, the event stream, logs, or API responses, and deletes them once the action is done. You can't pass inputs when you reject.
Input requests
When the agent needs something from a person, like a code, a link, or an account
detail, it comes through as an approval too. The action.capability is
input.provide and the question is in action.consequence. One approval screen
handles both.