Native Shell
Native Shell은 모든 플랫폼의 앱 안에서 만들어지는 AI UI입니다
Waymark은 순수 비동기 파이썬으로 작성된 내구성 백그라운드 워크플로를 실행합니다
Waymark runs durable background workflows written as plain async Python. It compiles and verifies your workflow up front, then runs it durably on Postgres with no replay. If it compiles, it runs in pr
개발 도구Rust · PythonGitHub @piercefreeman온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
SNS에서 화제 · 지금 3위 · 출처 1곳
11월 7일 11:16 확인 · 점수는 각 플랫폼이 보여 주는 값을 마지막 확인 시각에 옮긴 것입니다. 글 본문과 댓글은 옮기지 않고, 원문이 지워지면 여기서도 빠집니다.
🧭 Distributed background workflows, durably executed
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗waymark is a library to let you build durable background tasks that withstand server restarts, task crashes, and long-running jobs. It's built on Postgres, with no other infrastructure to deploy. Python is the first supported language; more are coming.
Documentation: waymark.sh
Getting started:
Running Waymark:
Python:
We ship all client and server binaries in one Python package. Install it via your package manager of choice:
uv add waymark
Let's say you need to send welcome emails to a batch of users, but only the active ones. You want to fetch them all, filter out inactive accounts, then fan out emails in parallel. Actions are the distributed work - plain async functions, sent to your workers:
from typing import Annotated
from waymark import Depends, action
@action
async def fetch_users(
user_ids: list[str],
db: Annotated[Database, Depends(get_db)],
) -> list[User]:
return await db.get_many(User, user_ids)
@action
async def send_email(
to: str,
subject: str,
emailer: Annotated[EmailClient, Depends(get_email_client)],
) -> EmailResult:
return await emailer.send(to=to, subject=subject)
The workflow is the durable control flow that orchestrates them:
import asyncio
from waymark import Workflow, workflow
@workflow
class WelcomeEmailWorkflow(Workflow):
async def run(self, user_ids: list[str]) -> list[EmailResult]:
users = await fetch_users(user_ids)
active_users = [user for user in users if user.active]
results = await asyncio.gather(
*[
send_email(to=user.email, subject="Welcome")
for user in active_users
],
return_exceptions=False,
)
return results
Run the node - the process that executes workflows and their actions - against your database:
export WAYMARK_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/waymark
export WAYMARK_HTTP_ENABLED=true # the webapp, on http://localhost:24119
uv run waymark-executor
Then kick off the workflow from any Python process, and wait for its result:
async def welcome_users(user_ids: list[str]):
await WelcomeEmailWorkflow().run(user_ids)
None of this executes inline in your webserver: the run is queued to Postgres and executed by the node.
Actions are the distributed work: network calls, database queries, anything that can fail and should be retried independently.
Workflows are the control flow: loops, conditionals, parallel branches. They orchestrate actions but don't do heavy lifting themselves.
Workflows are plain async Python. A few of the things they can do:
Retries and timeouts, per call. By default an action runs once, like a regular function call. Wrap a call in self.run_action(...) to give it a retry policy and a timeout - see Retries & Timeouts.
from datetime import timedelta
from waymark import RetryPolicy
async def run(self, order_id: str) -> Receipt:
return await self.run_action(
charge_card(order_id),
retry=RetryPolicy(attempts=5, backoff_seconds=10),
timeout=timedelta(minutes=2),
)
Branches and loops. if/elif/else, for and while compile into the workflow program and are executed by the runtime, just like your actions.
async def run(self, order_ids: list[str]) -> list[str]:
shipped = []
for order_id in order_ids:
status = await fetch_status(order_id)
if status == "shipped":
shipped.append(order_id)
return shipped
Parallel fan-out and durable sleep. asyncio.gather runs actions in parallel; asyncio.sleep pauses the workflow durably, surviving restarts - for a second or for a day.
import asyncio
async def run(self, user_id: str) -> str:
profile, history = await asyncio.gather(
fetch_profile(user_id),
fetch_purchase_history(user_id),
return_exceptions=False,
)
# wait a day before following up
await asyncio.sleep(24 * 60 * 60)
return await send_recommendations(profile, history)
Schedules. Run a workflow on a cron expression or a fixed interval, with no extra infrastructure - see Scheduled Workflows.
await schedule_workflow(DataSyncWorkflow, schedule_name="hourly", schedule="0 * * * *")
A built-in webapp. waymark-executor serves a webapp showing every workflow instance, each one's timeline of calls, and the load on your nodes.
Background jobs in webapps are so frequently used that they should really be a primitive of your fullstack library: database, backend, frontend, and background jobs. Otherwise you're stuck in a situation where users either have to always make blocking requests to an API or you spin up ephemeral tasks that will be killed during re-deployments or an accidental docker crash.
After trying most of the ecosystem in the last 3 years, I believe background jobs should provide a few key features:
On the point of control flow, we shouldn't be forced into a DAG definition (decorators, custom syntax). It should be regular control flow just distinguished because the flows are durable and because some portions of the parallelism can be run across machines.
Nothing on the market provides this balance - waymark aims to try. We don't expect ourselves to reach best in class functionality for load performance. Instead we intend for this to scale most applications well past product market fit.
Waymark takes a different approach from replay-based workflow engines like Temporal or Vercel Workflow.
| Approach | How it works | Constraint on users |
|---|---|---|
| Temporal/Vercel Workflows | Replay-based. Your workflow code re-executes from the beginning on each step; completed activities return cached results. | Code must be deterministic. No random(), no datetime.now(), no side effects in workflow logic. |
| Waymark | Compile-once. Your workflow's source (any supported language, e.g. Python) → intermediate representation → bytecode for a durable virtual machine. Your code never re-runs. | Code must use supported patterns. But once compiled, the runtime always knows exactly where the workflow is in its execution. |
The first time a workflow is run or scheduled, it is compiled to an intermediate representation (IR); in Python, the SDK parses its run() method's AST. This IR captures your control flow - loops, conditionals, parallel branches - and is lowered to bytecode for a durable virtual machine. The Rust runtime executes the bytecode and snapshots the VM's state to Postgres as it goes, so a workflow resumes from where it was after a crash. Your original workflow definition is never re-executed during workflow recovery.
This is convenient in practice because it means that if your workflow compiles, your workflow will run as advertised. There's no need to hack around stdlib functions that are non-deterministic (like time/uuid/etc) because you'll get an error on compilation to switch these into an explicit action. The few patterns that currently slip past the compiler are listed under known issues and tracked in #772.
When should you use Waymark?
Performance is a top priority of waymark. That's why it's written with a Rust core and runs continuous benchmarks on CI. But it's not the only priority. After all there's only so much we can do with Postgres as an ACID backing store. Once you start to tax Postgres' capabilities you're probably at the scale where you should switch to a more complicated architecture.
When shouldn't you?
There is no shortage of robust background queues, including ones like Temporal.io/RabbitMQ that scale to millions of requests a second.
Almost all of these require a dedicated task broker that you host alongside your app. This usually isn't a huge deal during POCs but can get complex as you need to performance tune it for production. Cloud hosting of most of these are billed per-event and can get very expensive depending on how you orchestrate your jobs. They also typically force you to migrate your logic to fit the conventions of the framework.
Open source solutions like RabbitMQ have been battle tested over decades & large companies like Temporal are able to throw a lot of resources towards optimization. Both of these solutions are great choices - just intended to solve for different scopes. Expect an associated higher amount of setup and management complexity.
[!IMPORTANT] Right now you shouldn't use waymark in any production applications. The spec is changing too quickly and we don't guarantee backwards compatibility before 1.0.0. But we would love if you try it out in your side project and see how you find it.
If you have a particular workflow that you think should be working but isn't yet compiling correctly, please file an issue.
The webapp is compiled into waymark-executor, so building from source requires Node.js (version in .node-version) and npm:
make js-deps
cargo build --bin waymark-executor
If you want to contribute, check out the contributing guidelines.