Agents
Agent work
Since backend 0.10.0 (CLI 0.11.0, Android 0.9.1), Dayhand has four features for AI agents that work on the plan next to the person who owns it: an activity log, acceptance criteria with proof of completion, dependencies with a blocked reason, and time-boxed claims. Backend 0.11.0 (CLI 0.12.0, Android 0.10.0) adds four that keep the owner in charge of those agents: approval requests decided on the phone, a switch that pauses every agent with a policy per tool, an inbox for proposed work, and a plan health report. Backend 0.12.0 (CLI 0.14.0) adds guides for each phase of a task, an agent brief for the start of a session, and Backlog.md import and export. This page explains the problem each one solves, the decisions behind it and the public designs it follows. The tool reference is in mcp.md, the REST routes in api.md and the commands in cli.md.
The problem
Dayhand started as one person’s day plan with an agent as a helper: the agent read Today, moved a task, left a resume note. Once several agents work on the same plan, for hours and with pauses, four things go wrong:
- Catching up is expensive. An agent that comes back after a pause has to read the whole plan again to see what changed, which costs time and context, and it still cannot tell who changed what.
- “Done” is a feeling. An agent marks a task done when it believes it is done. The person cannot see what done meant or what shows that it happened.
- Order is invisible. Some tasks can only start after others, and some are stuck on something outside the plan. Without that in the data, an agent picks work it cannot finish.
- Two agents take the same task. Nothing says “someone is on this”, so two agents start the same work, or one overwrites the other’s changes.
One rule shaped every answer: no new statuses. The board has three columns, todo, doing
and done, and the Android app, the CLI, the calendar feed, the webhooks and integrations rely on
exactly these three. Everything below is fields next to the status, so a client that does not
know them keeps working, and every addition is a minor version of the API (new tools, new
fields, new error codes; see mcp.md).
1. The activity log
list_events (and the resource dayhand://activity, dayhand log in the CLI, GET /api/v1/events)
returns the user’s task changes in the order they happened, each with who made it (by: app,
cli or agent: <name>) and the changed fields before and after. Every result carries a
nextCursor. An agent keeps it and passes it back as since next time: it then gets only the
changes made in between, without reading the plan. by and taskId narrow the log; hasMore
says another page is already waiting.
Decisions:
- The data was already there. Every change has written a history row since 0.1.0
(
taskEvents, signed with the actor). The log is a read of that table, not a second copy. - A position, not a time. Times are not enough as a cursor: two changes can share a
millisecond, and a change whose transaction started earlier can commit after a later one, so a
reader that resumes “after 12:00:00.123” can miss it. Each task change therefore gets a
position (
seq) from a per-user counter that every task change reads and writes in its own transaction. Convex runs such conflicting mutations one after the other, so positions follow the commit order and a cursor never skips a change. The position starts from the change’s time in microseconds, so a time can still be turned into a starting point (sincealso takes Unix milliseconds or an ISO 8601 time) and history from before 0.10.0 can be numbered into the same order by a migration (migrations:backfillEventSeqs, deploy.md). - Without
since, the newest changes. A new agent wants to start following the log from now, not from the first change ever made;since: "0"reads from the start. - Only task changes. History also records account changes (API keys, webhooks, devices); they
stay out of the log, which a key with scope
readmay read. - Bounded work per call. A call looks at no more than a few thousand changes; when filters skip most of them, the cursor continues from where it stopped.
Public counterparts: an MCP resource
with a URI template (dayhand://activity{?since}) for
clients that read context without a tool call; the cursor-and-hasMore shape of MCP’s own
pagination;
the same events as the webhooks (Standard Webhooks)
for clients that would rather be told than ask.
2. Acceptance criteria, plan and proof
A task may carry acceptance, a list of criteria { id, text, met }, and a plan, free text on
how the work will be done. complete_task takes an optional proof: { summary, links, checks },
where each check is { name, result: "pass" | "fail", url } (a test run, a CI job, a review). The
proof is kept on the task.
The rule: when a task has acceptance criteria and an agent completes it, every criterion must be
met and the proof must have a summary; otherwise the call fails with precondition_failed and
nothing changes. A person completing a task in the app or with a CLI key does it as before:
the rule binds agents, not the owner.
Decisions:
- Criteria are data, with ids. Like the checklist, criteria keep their ids (
a1,a2, …) across edits, so an agent ticksa2without rewriting the others’ text. They are separate from the checklist because they mean something else: the checklist is steps, the criteria are the test. - Agents tick criteria themselves, with
update_task. Dayhand does not judge whether a criterion is really met; it makes the claim explicit and leaves a record of who made it and with what proof, in the history and on the task. - A failing check does not block completion. The rule asks for met criteria and a summary; a
failcheck is recorded as it is, so the person sees it. precondition_failed, notinvalid_argument. The arguments are fine; the task’s state does not allow the change yet. The same code already meant “the state is not what this change needs” forIf-Match(RFC 9110 412).- A repeating task’s next occurrence keeps the criteria (unticked) and the plan, but not the proof.
Public counterpart: Backlog.md, a Markdown task board for people and AI agents, gives its tasks acceptance criteria and an implementation plan in the same spirit. Dayhand’s criteria are structured fields behind its own tools; file-format interchange is a separate import/export operation.
3. Dependencies and a blocked reason
dependsOn lists the tasks (ids, or numbers as "#42") that must be done first; blockedReason
says why a task cannot go on. list_all takes ready: true: tasks that are not done, not
blocked, and whose every dependency is done (ready: false gives the rest).
Decisions:
- Blocked is a reason, not a status. A blocked task stays in
todoordoing; the reason is what a person needs to read anyway. - No cycles. A task cannot depend on itself, directly or through others; such a change is an
invalid_argument. - A dependency in the trash, or purged, no longer holds a task back: it is not part of the plan any more. Restoring it makes it count again. Ready is worked out when asked, never stored.
- Ready does not look at claims. An agent picking work calls
list_allwithready: trueand thenclaim_task; aconflicttells it to take the next one.
For the Android 0.11.0 display of plans, dependencies and blocked reasons, including the limits of the phone’s dependency lookup, see Android screens.
Public counterpart: Backlog.md’s dependencies field, a list of task ids a task waits for. A plan
moved between the two keeps the same meaning: wait until those tasks are done.
4. Time-boxed claims
claim_task claims a task for a while (120 minutes unless minutes says 1 to 1440). The task then
shows claimedBy (the API key or agent connection, and its name) and claimExpiresAt. While the
claim lasts, another agent’s claim and changes of that task fail with the new error code
conflict (HTTP 409 on the REST API, exit code 6 in the CLI). renew_claim extends the claim
from now; release_task ends it, optionally with a checkpoint note saved as the resume note (where
the work stopped, the next step). Completing the task ends the claim too. After claimExpiresAt
anyone may claim the task again, without waiting for the holder.
The owner wins. The person’s own changes from the app and the CLI always go through, claim or
not, and the app can take a task over at once (app.release, the same operation with the owner as
the actor). The phone’s claim display and confirmation flow are described in
Android screens.
Decisions:
- A lease, not a lock. A lock held by a crashed agent blocks the task until someone notices. A lease ends by itself; the holder has to renew it while it works. The expired claim stays on the task until it is released or claimed again, so the history shows who last worked on it.
- The holder is the credential. A claim belongs to one API key or one agent connection, not to a name: two keys with the same name are two holders. Give each agent its own key.
- Other agents are refused, not only told. A claim that other agents could ignore would protect
nothing; their changes of the task fail with
conflictand the message says who holds it until when. Changes that only touch the task on the side (another task becoming the now task) are not refused. - Renewing a claim you no longer hold is
precondition_failed; renewing after it expired works as long as nobody else claimed the task meanwhile. - Claims do not change the status. Claiming does not move a task to
doing; an agent that starts the work says so withset_status, as a person would. - Renewals are not webhook events. Claiming and releasing are
task.updated; a renewal only moves the expiry and would flood the receivers of an agent that renews every few minutes.
Public counterparts: a Kubernetes Lease
names its holder (holderIdentity), its length (leaseDurationSeconds) and when it was renewed,
and anyone may take it over once it has run out; Amazon SQS’s
visibility timeout
hides a message from other consumers while one works on it, can be extended
(ChangeMessageVisibility) and gives the message back when it runs out. Dayhand’s claim is the
same contract for a task, with the owner allowed to step in. The status code follows RFC 9110
409 Conflict.
The second problem: who is in charge
The first four features let agents work together. Once they do, for hours and while the owner is away from the desk, the owner needs four more things:
- A say before something risky. An agent about to deploy, delete or pay should be able to ask, and the owner should be able to answer from the phone, without a terminal.
- A brake. When an agent misbehaves, or the owner just wants quiet, one control has to stop every agent at once, and finer rules have to say which agent may use which tool.
- A door for work nobody asked for. Alerts, issues and agents’ own ideas should not land in Today as if the owner had planned them, and the same alert must not make ten tasks.
- One look at the state of things. What is late, what is stuck, who is working on what, and what was finished without anything to show for it.
The same rule holds: no new statuses. Everything below is new tables, new fields of a task and new tools; a client that knows none of them keeps working.
5. Approval requests
request_approval stores a request: what it is about (optionally a task), what the agent wants
to do (action, with detail), 2 to 4 options, each with the consequence of picking it,
whether the step is reversible, and how long the owner has (expiresInMinutes, a day unless
set, a week at most). The phone shows it under Settings → Agents (waiting for you), on the health
card above Today, and as a notification; the owner picks an option and may add a note. The agent reads
the outcome with get_approval (pending, decided or expired), or is told by the webhook
approval.decided. dayhand ask and dayhand approval do the same from a terminal, and a cli
key can decide with dayhand approvals decide.
Dayhand records the decision; it does not carry it out and cannot stop an agent from acting
without one. The agent, or the tooling around it, has to ask and then respect the answer. What
Dayhand guarantees is the record: who asked, what they offered, what the owner chose, when, in the
task’s history and the activity log. The one place Dayhand does enforce a decision is its own
tools, through the tool policy ask (next section).
Decisions:
- Options, not yes or no. Real decisions often have a third way (“wait until Monday”). The
agent offers the choices it can actually carry out and says what follows from each, so the owner
decides without asking back. Option ids default to
o1,o2, …, so an agent can branch on them. - Requests expire. A question that waits forever is a decision by default. After
expiresAtthe request isexpiredand can no longer be decided; the agent has to ask again. Expiry sends no webhook: the agent knows the time. - The owner decides, nobody else.
decide_approvalis one of the owner’s tools: agents do not see it, and an agent that calls it getsforbidden. Deciding again the same way changes nothing; deciding differently isprecondition_failed. - A request about a task is part of that task’s story: asking and deciding enter its history
and the activity log (
approvalRequest,approvalDecide). - Closed app. The phone looks for new requests in the background every 15 minutes (Android’s
shortest periodic work) and shows a notification for each. A request that needs an answer in
less than that will often be seen only when the owner opens the app; the agent should give a
realistic
expiresInMinutes. There is no push service in between, on purpose: Dayhand has no server of its own that could reach the phone.
Public counterpart: MCP elicitation lets a server ask the user something through the client, during one request. Dayhand’s requests are the asynchronous form of the same idea: the agent asks, goes on with something else or waits, and the answer comes from another device, maybe hours later. The webhook follows Standard Webhooks, like every other Dayhand event.
6. Pausing agents and tool policies
Pause agents (Settings → Agents on the phone, dayhand agents pause, pause_agents) is one
switch. While it is on, every change by an agent key or an agent connection (OAuth) is refused
with agents_paused, including dry runs, approval requests and the account routes of the REST
API (keys, webhooks, import); reads keep working, so an agent can still see what is going on and
tell the user. Switching it off lets them change things again. The owner’s own changes, from the
app and from cli keys, are never refused.
Each agent key can also have a policy: for a tool that changes the plan, or for "*"
(every such tool without its own rule), allow (the default), ask or deny
(dayhand policy set <key> <tool> <rule>, set_tool_policy; phone controls).
deny refuses the call with forbidden. ask refuses it with approval_required and the id of
an approval request Dayhand made for exactly that call; once the owner picks “Allow once”, the
agent repeats the call with approvalId and it runs, once. Reading tools have no policy.
Decisions:
- Checked where the change happens. The pause and the policies are checked inside the same
transaction as the change (
gateAgentWriteinconvex/lib/agentControls.ts), for MCP and the REST API alike. There is no way around them through another surface, and no window between the check and the write. - The pause is not a revocation. Revoking keys would stop agents too, but they would need new keys afterwards and their history would split. A pause keeps every key and connection and only holds their changes back.
- An agent cannot lift its own brake. Pausing, resuming and setting policies are the owner’s
tools: hidden from agents, and
forbidden(or, while paused,agents_paused) if called. - One approval, one call. A policy request is bound to the key, the tool and the arguments
(their SHA-256, without
clientId,dryRunandapprovalId), and is used up in the transaction that applies the change; if the change fails, the approval stays unused. Calling again while the request waits returns the same request, so a retrying agent does not flood the phone. A policy request waits an hour. - A dry run needs no approval under
ask: it changes nothing, and it is how an agent shows the owner what it is about to do. Underdenyand while paused it is refused like the call. - Policies are per key. An agent connection (OAuth) has its scopes (
tasks:read,tasks:write) and obeys the pause, but has no per-tool rules; give an agent that needs finer rules its own API key.
Public counterparts: Claude Code’s permission rules sort tools into allow, ask and deny; Dayhand uses the same three words with the same meaning, per agent key and per Dayhand tool, with the “ask” answered on the phone. The pause is the kill switch of operations: one long-lived toggle that turns a whole capability off without a deploy.
7. The proposal inbox
A task added with proposal: true is a proposal: it has proposedAt and waits in the
owner’s inbox (Settings → Agents → Proposals, the health card, dayhand proposals). It is not
in Today, the board, list_all (unless asked for with proposed: true), the ready list, the
calendar feed or the phone’s reminders until the owner decides:
- Accept: it joins the plan as it is.
- Merge into a task already in the plan: that task gets the proposal’s note (after a line
naming it), links, labels, source and return webhooks. Since backend 0.13.0, it also inherits
the proposal’s project if the target has none and that project still exists and is not archived;
otherwise its project stays unchanged. The proposal goes to the trash with
mergedInto. - Reject: it goes to the trash, restorable for 30 days.
Work waits for the decision: a proposal cannot be claimed, made the now task, moved to doing or
completed. Only the owner decides.
Any new task can carry three more fields:
source{ kind, ref, url? }: where it came from, e.g.{ kind: "sentry", ref: "CHECKOUT-42" }. One source makes one task: adding a task with a source that already made one returns that task instead, also when it was rejected (it is in the trash) or merged (the source then leads to the task it was merged into). Once the trash is purged, the source is free again.requestedBy: who asked for it, in words (a customer, a colleague). An agent’s proposal without it saysagent: <name>.returnTo: the id of one of the user’s webhooks. When the task is completed, rejected or merged, that webhook getstask.returnedwith the outcome and the task’s proof (section 2), whatever events it subscribes to. An integration that opened a task from an alert can close the alert with the proof the work left.
Decisions:
- A field, not a status. A proposal is a
todotask withproposedAt; accepting removes the field. Clients that do not know proposals never see them, because every list they read leaves proposals out. - The creator chooses. An agent or integration says
proposal: truefor work nobody asked for; work the owner asked for goes straight in as before. - Uniqueness by source, kept in its own table (
taskSources), so it holds across merges and the trash, and two calls with the same source at the same moment still make one task (Convex runs them one after the other). - Rejecting is not purging. A rejected proposal sits in the trash for 30 days, so a mistaken rejection can be undone and the same alert does not come back as a new task the next minute.
- The return target is a registered webhook, not a free URL: its address was checked when it was added (public HTTPS only) and its messages are signed with its own secret.
Public counterpart: the Idempotency-Key
draft for HTTP makes a repeated request create one resource. A task’s source is the same promise
keyed by the outside world’s own id, so it also holds when the sender is not the one retrying, e.g.
an alert that fires again.
8. The plan health report
plan_health (also GET /api/v1/health, dayhand health, the MCP prompt plan_health) answers
“what needs a look?” in one read: overdue tasks, tasks due today, tasks in progress and who works
on them (agent: <name> while a claim lasts), claims that ran out, blocked tasks with every
reason (the blocked reason and each dependency not done yet), tasks waiting for an approval and
the pending requests in all, proposals in the inbox, tasks completed today with and without
proof, and the oldest open task with its age in days. Each section has its count and lists the
first 20 tasks. Whether agents are paused comes first.
On the phone, a card above Today says the same counts in one line, the most pressing first, and
its Review opens Settings → Agents when a request or a proposal waits. The phone reads the
report from the server (app:planHealth) when Today opens, when the app comes back to the front
and when the tasks change; offline it keeps the last report, and a phone without a server has no
card.
Decisions:
- Counts and names, not judgements. The report says what is there; the agent or the owner
decides what to do about it. The
plan_healthprompt asks the agent to go through it with the owner, most urgent first, and to ask before changing anything. - Open means in the plan: not done, not archived, not in the trash and not a proposal.
- “Done without proof” counts tasks completed today with no summary, link or check. It is a prompt to look, not an error: people complete tasks without proof all the time.
The third problem: starting well, and plans kept elsewhere
With eight features an agent has a lot to remember, and two more gaps show:
- Where to begin. An agent that starts a session reads Today, then its claims, then the approvals it asked for, then works out what to do next: several calls before any work. And the rules for one task are spread over one long guide.
- Plans in files. Many people who work with coding agents keep tasks as Markdown files in the repository, in the format of Backlog.md. Moving such a plan into Dayhand, or out of it, should not mean retyping it.
9. Phase guides and the agent brief
Three MCP resources split the work on one task into phases, each a short page that names the tools to use:
dayhand://workflow/intake: before work. Is the task already there (search,source)? Asked for, or a proposal? Acceptance criteria that can be checked, a plan written just before starting, dependencies and a blocked reason, a dry run before many changes.dayhand://workflow/execution: during work. Choose ready work,claim_taskandrenew_claim, tick criteria as they become true,request_approvalbefore a risky step, a checkpoint when stopping, what each refusal means.dayhand://workflow/finalization: at the end. Check the criteria again,complete_taskwith proof (a failed check recorded as failed), the owner’s yes before a last step that needs it, or hand the task back with a note.
agent_brief (dayhand prime --json, GET /api/v1/brief) is the start of a session in one
read: every field of list_today, then whether agents are paused and the caller’s own tool
policies, the claims in force and those that ran out (each marked mine when the caller holds
it), the approval requests waiting and the owner’s decisions on the caller’s requests in the last
day, the first tasks ready to work on, and nextSteps: sentences, most pressing first, such as
“You hold #12 … until 14:30: renew_claim while you work …”. dayhand prime prints the same as
text before the rules of the road.
Decisions:
- Phases, not one longer guide.
dayhand://guidestays the reference; the phase guides are what an agent rereads at the point it needs them. They are static text, the same for every user, so they need no user data, count against no limit and may be cached publicly. - The brief is list_today with more fields.
dayhand prime --jsonprintedlist_todaybefore; it now prints a superset, so a script that read it keeps working (fields may be added, mcp.md). Against a backend before 0.12.0,dayhand primefalls back tolist_today. - From the caller’s side. The same plan reads differently for two agents: each sees its own
claims and requests as
mine, and ready work leaves out tasks another agent holds. The owner’s decisions are listed only to the agent that asked. - Next steps are sentences, not commands. They name tools and resources but do not act; an agent still decides. While agents are paused there is one step: stop changing things.
Public counterparts: Backlog.md also offers its agent instructions as MCP resources, split into a workflow overview and guides for creating, executing and finalizing a task; Dayhand’s guides cover the same three phases with Dayhand’s own tools and texts. MCP resources are the way to give a client context without a tool call.
10. Backlog.md import and export
import_backlog (dayhand backlog import <dir>) reads Backlog.md task files into ordinary
Dayhand tasks; export_backlog (dayhand backlog export <dir>) writes the plan as task files.
Import uses the source identity shared by integrations, not a second
synchronization mechanism. Its report explains what was created, skipped or refused, and a dry
run previews that report without storing anything.
See Backlog.md import and export for the field mappings, source normalization, call limits, approval handling, export safety and overflow warnings, and the format’s license and attribution.
What an agent does
- Start of a session:
agent_brief(dayhand prime); back from a pause:list_eventswith thenextCursorit kept;plan_healthfor the whole plan. - Pick work:
list_allwithready: true. claim_task; onconflict, pick another task.- Write
acceptanceandplanif the task has none;renew_claimwhile working. - Before a risky step:
request_approval, then act on whatget_approvalsays. - Tick the criteria with
update_task, thencomplete_taskwithproof. - Stopping early:
release_taskwith a checkpointnote; stuck:blockedReason. - Work nobody asked for:
add_taskwithproposal: trueand asource. agents_paused: stop changing things and wait.forbidden: do not retry.approval_required: wait for that request, then repeat the call withapprovalId.
The same steps are in the dayhand://guide resource, one phase at a time in
dayhand://workflow/intake, /execution and /finalization, and in dayhand prime for agents.
Not in this release
Backlog.md import and export are copies, not sync: a change made on one side afterwards stays there, and importing a changed file again does not update its task. For the phone’s tool-policy controls and their limits, see Android screens. The phone notifies about approval requests at most every 15 minutes while the app is closed.
This page is docs/agent-work.md in the repository, where
you can suggest a change.