Ready to put this into action?
Get the complete AI Integration Playbook β Practical AI implementation guide β prompt engineering, workflow automation, and ROI frameworks.
Article 120 Β· Part 12
Add Approval Steps Before Important Actions
Bind permission to the exact action a person reviewed, and enforce that boundary where execution happens.
By Randy Salars Β· Published
On this page
- Identify the actions that need explicit control
- Define the approval object
- Make states and transitions explicit
- Run a local approval-boundary exercise
- Understand what the digest does
- Inspect the tested outcomes
- Use a transaction for the decision and local effect
- Do not confuse an outbox record with external delivery
- Handle changes, revocation, and expiry as product behavior
- Make review easy enough to be effective
- A reusable prompt
- For students: simulate authority without contacting anyone
- Practice: bind a reviewed proposal to one local effect
Bind permission to the exact action a person reviewed, and enforce that boundary where execution happens.
The coordinator approves a workshop reply asking a learner to confirm a date. Before the workflow sends it, an assistant rewrites the message to say the room is booked.
The workflow still displays an approval badge. But the approved message and the proposed send are no longer the same action.
An approval step is useful only when it connects the reviewer, target, content, version, and limits to the operation that actually runs. A button or a reassuring sentence cannot establish that connection by itself.
Identify the actions that need explicit control
Important actions can include sending communications, purchasing, deleting, deploying, changing permissions, or updating authoritative records. The necessary approval depends on the task and existing authorization.
Routine work already covered by a clear standing instruction may proceed within that instruction's scope. A materially different recipient, commitment, amount, or data access may require a new decision. The aim is to make authority precise, not to ask the same question repeatedly.
For this lesson, the learning center requires a reviewer to approve the exact reply before a separate execution role records a simulated send. No real messaging service is involved.
Define the approval object
The proposal should contain every field that can materially change the action: operation type, target, content, version, and relevant limits. For an event, include time, zone, attendees, and notification behavior. For a purchase, include the item, seller, quantity, total limit, and delivery details required for the transaction.
The reviewer must see those significant fields. A cryptographic digest can bind the stored approval to a precise representation, but a hash alone is not a useful human preview. Show the actual action and retain a digest of the same canonical content.
OWASP's transaction-authorization guidance discusses binding authorization to significant transaction data and enforcing the decision on the server. The same principle applies to the fictional draft in this lesson.
Make states and transitions explicit
A proposal begins unapproved. An authorized reviewer can approve its current version for a limited period. The executor checks the approval immediately before the permitted operation. A changed proposal requires renewed review. A used approval cannot create a second effect.
A small state map helps clarify the branching:
Diagram sourcestateDiagram-v2
[*] --> Proposed
Proposed --> Approved: Exact proposal reviewed
Approved --> Proposed: Material change
Approved --> Expired: Time limit reached
Approved --> Revoked: Authorization withdrawn
Approved --> Recorded: Valid execution
Recorded --> Recorded: Replay returns existing result
This diagram describes the intended workflow. The code below stores a state plus an expiry time; expiry is checked at execution rather than requiring a background job to rewrite the state field. Revocation is a design consideration, not an exposed operation in this small implementation.
Run a local approval-boundary exercise
Save the following as approval_lab.py and run it with Python 3.9 or later. It creates a temporary SQLite database and records synthetic outbox rows. It never contacts an email service.
The actor names and integer times are trusted test fixtures. A real application must derive identity from its authentication system and obtain time from a trusted server clock. A caller must not gain a role merely by sending the string reviewer or runner.
from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
import hashlib
import hmac
import json
from pathlib import Path
import sqlite3
import tempfile
import threading
@contextmanager
def transaction(path):
connection = sqlite3.connect(path, timeout=5, isolation_level=None)
try:
connection.execute("BEGIN IMMEDIATE")
yield connection
connection.commit()
except Exception:
connection.rollback()
raise
finally:
connection.close()
def encode(action):
fields = {"action_id", "operation", "target", "body", "version"}
if not isinstance(action, dict) or set(action) != fields:
raise ValueError("Unexpected action fields")
if (action["operation"] != "simulated_send" or type(action["version"]) is not int
or action["version"] < 1):
raise ValueError("Invalid operation or version")
for field in ("action_id", "target", "body"):
if not isinstance(action[field], str) or not 1 <= len(action[field]) <= 500:
raise ValueError("Invalid action text")
if not action["target"].endswith("@example.invalid"):
raise ValueError("Only fictional targets are allowed in this lab")
payload = json.dumps(action, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
return payload, hashlib.sha256(payload.encode("utf-8")).hexdigest()
def prepare(path, action):
payload, digest = encode(action)
with transaction(path) as connection:
connection.execute("INSERT INTO actions (id, payload, digest, state) VALUES (?, ?, ?, 'proposed')",
(action["action_id"], payload, digest))
return digest
def approve(path, action_id, reviewed_digest, actor, now):
if actor != "reviewer":
raise PermissionError("Approver identity required")
with transaction(path) as connection:
row = connection.execute("SELECT digest, state FROM actions WHERE id = ?", (action_id,)).fetchone()
if row is None or row[1] != "proposed" or not hmac.compare_digest(row[0], reviewed_digest):
raise PermissionError("The reviewed proposal does not match")
connection.execute("UPDATE actions SET state='approved', approved_by=?, expires=? WHERE id=?",
(actor, now + 60, action_id))
def execute(path, action, actor, now):
if actor != "runner":
raise PermissionError("Executor identity required")
payload, digest = encode(action)
with transaction(path) as connection:
row = connection.execute("SELECT digest, state, expires FROM actions WHERE id=?",
(action["action_id"],)).fetchone()
if row is None or not hmac.compare_digest(row[0], digest):
raise PermissionError("Proposal changed or approval missing")
if row[1] == "used":
return "already_recorded"
if row[1] != "approved" or row[2] <= now:
raise PermissionError("No current approval")
connection.execute("INSERT INTO simulated_outbox VALUES (?, ?, ?)",
(action["action_id"], payload, actor))
connection.execute("UPDATE actions SET state='used' WHERE id=?", (action["action_id"],))
return "recorded"
def main():
with tempfile.TemporaryDirectory(prefix="approval-lab-") as folder:
path = Path(folder) / "lab.sqlite"
with transaction(path) as connection:
connection.execute("CREATE TABLE actions (id TEXT PRIMARY KEY, payload TEXT, digest TEXT, "
"state TEXT, approved_by TEXT, expires INTEGER)")
connection.execute("CREATE TABLE simulated_outbox (action_id TEXT PRIMARY KEY, payload TEXT, executed_by TEXT)")
def proposal(identifier):
return {"action_id": identifier, "operation": "simulated_send", "version": 1,
"target": "[email protected]", "body": "Please confirm the Tuesday date."}
def rejected(function):
try:
function()
except PermissionError:
return
raise AssertionError("Expected rejection")
first = proposal("A1")
digest = prepare(path, first)
rejected(lambda: execute(path, first, "runner", 1000))
rejected(lambda: approve(path, "A1", digest, None, 1000))
rejected(lambda: approve(path, "A1", "0" * 64, "reviewer", 1000))
approve(path, "A1", digest, "reviewer", 1000)
rejected(lambda: execute(path, {**first, "body": "A changed commitment"}, "runner", 1001))
rejected(lambda: execute(path, {**first, "target": "[email protected]"}, "runner", 1001))
rejected(lambda: execute(path, {**first, "version": 2}, "runner", 1001))
rejected(lambda: execute(path, first, None, 1001))
assert execute(path, first, "runner", 1001) == "recorded"
assert execute(path, first, "runner", 1002) == "already_recorded"
expired = proposal("A2")
approve(path, "A2", prepare(path, expired), "reviewer", 1000)
rejected(lambda: execute(path, expired, "runner", 1060))
concurrent = proposal("A3")
approve(path, "A3", prepare(path, concurrent), "reviewer", 1000)
barrier = threading.Barrier(2)
def attempt(_):
barrier.wait(timeout=3)
return execute(path, concurrent, "runner", 1001)
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(attempt, range(2)))
assert sorted(results) == ["already_recorded", "recorded"]
with transaction(path) as connection:
assert connection.execute("SELECT count(*) FROM simulated_outbox").fetchone()[0] == 2
assert connection.execute("SELECT state FROM actions WHERE id='A2'").fetchone()[0] == "approved"
print("PASS: unapproved, identity, changed-content, expiry, replay, and concurrent-execution checks")
print("Two simulated outbox rows; zero messages sent")
if __name__ == "__main__":
main()
Understand what the digest does
The encode function accepts a fixed set of fields and produces a consistent JSON representation. Its digest changes when the target, body, version, or other included field changes.
Approval checks that the digest reviewed by the fictional reviewer matches the stored proposal. Execution independently computes the candidate's digest and compares it with that same stored record. This prevents a changed message from using the earlier approval in the tested system.
The digest is not a password, signature, or proof of identity. Anyone who knows the content can compute it. Authority comes from the trusted reviewer action and the protected database record, not from possessing the hash.
If an application stores the proposal and approval in a file the model can freely rewrite, this protection is weakened. The approval record must live behind controls appropriate to its role.
Inspect the tested outcomes
The example ran successfully during preparation. It rejected execution before approval, a missing approver identity, a mismatched reviewed digest, changed content, a changed recipient, a changed version, a missing executor identity, and an approval at its expiry boundary.
It then verified two successful behaviors. Replaying a used approval returned the existing-result status. Two simultaneous attempts for another approved action produced one recorded result and one already-recorded result.
The database ended with two simulated outbox rows: one for each successfully executed action. No actual message was sent. The expired action retained its stored approved label, but its expiry time prevented execution; that is why applications should interpret the state and time together.
These checks support the local transaction boundary. They do not verify a deployed sign-in system, network delivery, a production database cluster, or a messaging provider's duplicate-handling behavior.
Use a transaction for the decision and local effect
The executor reads the approval, checks it, records the simulated effect, and marks the approval used within one database transaction. The outbox's primary key also prevents two rows with the same action identifier.
SQLite's transaction documentation explains the behavior of BEGIN IMMEDIATE and write transactions. In this lab, concurrent writers are serialized around the approval check and local record creation. The second attempt sees the state left by the first committed attempt.
A separate βcheck approvalβ request followed much later by an unrelated write would leave room for changes between the two. Keep the relevant checks and local state transition together, using the guarantees offered by the actual storage system.
The five-second database timeout is also a boundary. If a lock cannot be acquired in time, the operation may fail; that failure must not be reported as a completed send. A real application needs an explicit response and recovery policy for that condition.
Do not confuse an outbox record with external delivery
A transactional outbox can record the intention to perform an external action. Another worker may then send it to an email service or other destination. That introduces another boundary.
If the provider accepts a message but the worker times out before recording success, the outcome is uncertain. Retrying without reconciliation can create a second send unless the destination supports an appropriate deduplication mechanism.
Therefore, the local result in this lesson is named recorded, not delivered. The next article develops failure recovery and duplicate prevention across systems. An approval gate and a transaction solve important parts of the problem, but they do not create universal exactly-once delivery.
A useful production record links the approved action to its outbox entry, provider operation reference, and observed completion state. Each link should be supported by actual execution evidence.
Handle changes, revocation, and expiry as product behavior
When someone edits a proposal after approval, show that renewed review is needed. Do not leave an old approval badge beside new content. Preserve the earlier version so the reviewer can see what changed.
Revocation needs a defined point of effect. Before local execution, a revoked approval should be rejected. After an external action has occurred, revocation cannot retroactively prevent that action. The application may need a separate correction or cancellation process.
Expiry should also be meaningful to the task. The lab's sixty-second window is a test value, not a general recommendation. A real window should reflect how quickly recipients, prices, availability, or other important conditions can change.
Some actions need additional conditions at execution. An approved purchase may still be invalid if the total exceeds its limit. An approved record edit may be stale if another user changed the record version. Include those conditions in the action contract and verify them at the authoritative boundary.
Make review easy enough to be effective
A preview should help a person detect the mistakes that matter. Show recipients, commitments, attachments, amounts, and changed fields prominently. Avoid burying the significant action in a long log.
For a revised draft, show a readable difference from the approved version. For a bulk operation, summarize the scope and provide a way to inspect exceptions and individual targets. Approval quality depends partly on whether the reviewer can understand what is being approved.
Record who approved what, when, and under which conditions. Keep the record proportionate and protect any sensitive content it contains. An audit trail should explain the action without becoming an unnecessary copy of every private input.
A reusable prompt
Design approval controls for this exact proposed action. Identify the reviewer, executor, target, content version, relevant limits, expiry, and revocation rules. Bind the reviewed proposal to execution in trusted application state. Test missing approval, changed fields, stale versions, replay, and concurrent attempts. Distinguish a locally recorded effect from confirmed external completion.
For students: simulate authority without contacting anyone
Students can use the complete lab to study state machines, hashing, transactions, and concurrency with fictional messages. The important exercise is explaining why a changed recipient invalidates approval even when the body remains unchanged.
For group projects, define which decisions require a team review and which routine actions already fall within a member's role. Avoid making approval a ceremonial click after the important decision has already been executed.
If AI helps propose a workflow, students should identify which rules are merely described in a prompt and which are enforced by the code. A prompt saying βalways ask firstβ is not equivalent to an execution function that rejects unapproved work.
Practice: bind a reviewed proposal to one local effect
Run the program. Identify where the proposal is prepared, approved, checked, and recorded. Explain why the two concurrent attempts produce only one outbox row for their shared action identifier.
Add a harmless change to the message body after approval and confirm that execution is rejected. Then create a new proposal for the changed content and follow the full review sequence rather than reusing the old approval.
Completion check: Unapproved, changed, expired, and improperly identified actions cannot execute in the tested system; a replay does not create another local effect; and the report does not confuse recorded intent with external delivery.
Stretch: Add a revocation operation and test a race between revocation and execution. Define which transaction wins and what result the other operation must report.
Get the AI Dispatch
Weekly insights on ai & technology β delivered to your inbox. No spam, unsubscribe any time.
Want to choose specific topics? Customize your interests