Skip to main content

holdpoint

CI PyPI Python

A hold point is a stage where work stops until someone authorized releases it. holdpoint gives your agent one.

The problem

Your agent drafts an email to a customer, and the draft is good — most of the time. The one time it is not, smtp.send() has already run. Every team that ships agents ends up writing the same layer by hand: park the risky call somewhere, let a human look at it, run it after approval. Hand-rolled versions tend to share the same bugs: the action runs twice when two reviewers click at once, approved work vanishes when the process restarts, and nobody can say afterwards who approved what.

holdpoint is that layer as a library: one primitive — a deferred function call waiting for a decision — with the concurrency, durability, and audit questions answered once. Zero runtime dependencies; state lives in a SQLite file you own.

Install

pip install holdpoint

Python 3.10+. Nothing else.

Example

from holdpoint import HoldPoint, SQLiteStore

hold = HoldPoint(store=SQLiteStore("approvals.db"))

@hold.guard(kind="send_email", ttl="24h")
def send_email(to: str, subject: str) -> str:
    print(f"sending {subject!r} to {to}")
    return "sent"

pending = send_email(to="ceo@corp.com", subject="Proposal")  # queued, NOT sent
print(pending.status.value)                                  # "pending"

for item in hold.pending():                                  # your dashboard, CLI, or bot
    result = hold.approve(item.id, actor="sava", patch={"subject": "Shorter"})
    print(result.value)                                      # "sent"

Calling the function stores the call. Approving executes it — once — with the reviewer's edits applied. Everything lands in an append-only audit trail in the same database file.

When you don't need this

Honesty first: holdpoint is a narrow tool, and often the wrong one.

  • Your agent runs on LangGraph, Pydantic AI, or the OpenAI Agents SDK. Use their native mechanisms (interrupt(), requires_approval=True, needs_approval=True). They pause the whole agent loop and resume it with the human's answer, which composes better inside those frameworks than an external queue does.
  • You want Slack/email approval UX out of the box. That is a product, not a library. HumanLayer sells exactly it. holdpoint gives you hooks and a queue; the notification channel is your code.
  • You need approvals across multiple services or machines. Use Temporal. Its signals + durable workflows are strictly stronger than a SQLite file, at the cost of running a cluster.
  • The action is cheap to undo. A soft-deleted row does not need a human gate. Add approval only where mistakes are expensive and irreversible.

holdpoint fits when you have a Python process, a handful of scary function calls, and no appetite for new infrastructure.

Features

Guard any function. @hold.guard(kind=...) works on def and async def, with positional-only params, *args, **kwargs, and defaults (captured at submit time, so the reviewer sees the complete call). Full type inference: the IDE knows send_email(...) returns Action[str].

Patch before running. The reviewer is not a yes/no button. approve(id, actor="sava", patch={"subject": "Shorter"}) edits arguments before execution; unknown keys and unserializable values are rejected with exact messages, and the audit records both the original payload and the patch.

hold.approve(item.id, actor="sava", patch={"amount": 90})

Exactly-once execution. Approval is a compare-and-set in the store. Fifty concurrent approvals of one action execute it once; the other forty-nine wait and return the same stored result. See Guarantees for the precise claim.

Crash recovery. If the process dies between "approved" and "executed", hold.recover() at the next start finishes the job — or, if the crash happened mid-execution, surfaces the ambiguity instead of guessing.

report = hold.recover()          # call at startup, before serving traffic

TTLs with policies. ttl="24h" plus on_expire="expire" | "auto_approve" | "auto_reject". No background threads: call hold.expire_due() on your own cadence; overdue actions are un-approvable regardless.

Auto-approval rules. A predicate can wave routine actions through so the human only sees the interesting ones. Rules run the same machinery and leave the same audit trail (actor="rule:<name>").

def small_amounts(kind, payload):
    if kind == "wire" and payload["amount"] < 100:
        return "approve"
    return None

hold = HoldPoint(store=..., rules=[small_amounts])

Deduplication. dedup_key=lambda p: p["email"] collapses repeated proposals of the same action while one is live; the same key with a different payload is a loud conflict, never a silent overwrite.

Immutable audit. Who submitted, who decided, what was patched, what ran, what it returned or raised — append-only, enforced by database triggers, with secrets redacted via redact=("password",).

Hooks. @hold.on_pending (and on_approved / on_rejected / on_expired) is where you wire Telegram, Slack, or anything else. Notifications are best-effort by design; the queue is the source of truth.

Know where things run. The process that calls approve() executes the function — your review bot needs the same imports, credentials, and network access as the agent would. Hooks run synchronously in the thread that caused the event; keep them fast or hand off to your own worker. Details in docs/concepts.md.

Async. await send_email(...), await hold.approve_async(...). Sync and async guards share one queue and one semantics.

Guarantees

What is promised, precisely:

  • At most one execution at a time, ever. Concurrent approvals — threads, event loops, separate processes on one machine — collapse to one execution via an atomic claim; the rest share its result.
  • Exactly one execution if the process survives the call. The normal case.
  • After a crash: an approved action whose execution never started is completed by recover() safely. An action that died mid-execution is ambiguous by nature — the default marks it failed with an explicit "side effect unknown" error for a human to investigate; opting into recover(interrupted="retry") chooses at-least-once, knowingly.
  • Every state change commits atomically with its audit entry. A crash cannot separate what happened from the record of it.

What is not promised: coordination across machines (single shared SQLite file, one host), guaranteed hook delivery (best-effort; poll the queue for truth), retries of failed actions (a failed action stays failed until a human re-submits), and protection from os._exit() between a side effect and its recording — that gap is physics, and holdpoint chooses to expose it rather than pretend.

Compared honestly

holdpoint LangGraph interrupt HumanLayer Temporal
Standalone (no framework/service) yes no no (SaaS) no (cluster)
Runtime dependencies 0 many 6 server + SDK
Durable queue you own SQLite file checkpointer their cloud their cluster
Edit args before run yes via resume value feedback only via signal
Exactly-once execute yes (see above) n/a no claim yes
Auto-rules / TTL / dedup yes build it partial build it
Immutable audit log yes no cloud logs full history (better)
Human notification UX your code your code built-in, better your code
Multi-node / distributed no via Postgres yes yes, much better
Pauses the whole agent loop in-place no yes, better in-framework yes yes

The bold cells in the last rows are where holdpoint loses. If those rows are your requirements, take the other column's tool.

Status

v0.1.x. The public API (everything importable from holdpoint) follows semver: breaking changes only with a major-version bump; 0.x minor bumps may extend but not break. The SQLite schema is versioned and migrates forward automatically; downgrade is refused explicitly.

License

FSL-1.1-ALv2 (Functional Source License): free for any use except competing commercial hosting, and each release becomes Apache-2.0 two years after publication. Contributions require the short CLA.

Metadata

Release files for holdpoint 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for holdpoint 0.1.0
File Size Uploaded
holdpoint-0.1.0.tar.gz 99.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for holdpoint 0.1.0
File Interpreter ABI Platform
holdpoint-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 143.7 kB

Release files / holdpoint-0.1.0.tar.gz

Download URL holdpoint-0.1.0.tar.gz
Size 99.5 kB
Tags Source
SHA-256 checksum
How to use checksums
31588d3388d06014c502639882b4842808f7c06bad815fb7a851b55e9b84605b
BLAKE2b-256 checksum
How to use checksums
84bcb95655fea82223908df1d923c1dcd8e24d4708c665412e609b3c923dbce2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.

Transparency log

Release files / holdpoint-0.1.0-py3-none-any.whl

Download URL holdpoint-0.1.0-py3-none-any.whl
Size 44.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
44a2b3ef8c1677d2528423abfbe003aacf0deba0806c5ec52b4ac70a648b0425
BLAKE2b-256 checksum
How to use checksums
7d30a0335dd967399f51eea4e073abe4f511ea6825a38c7d64cf7bceec6884c5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page