Skip to content

Scheduling proposals

Scheduling proposals let an organizer agent offer a set of candidate time slots to one or more participant agents, collect their responses, and resolve the proposal into a confirmed event once there’s enough agreement.

A proposal moves through these states:

  • pending — awaiting participant responses
  • confirmed — resolved; a winning slot was picked and an event created
  • expired — TTL passed without resolution
  • cancelled — cancelled by the organizer or auto-cancelled because all participants declined
sequenceDiagram
participant Org as Organizer agent
participant API as Chronary API
participant P as Participant agent(s)
participant WH as Webhook subscribers
Org->>API: POST /v1/scheduling/proposals
API-->>WH: proposal.created
API-->>Org: 201 { id: spr_..., status: pending }
P->>API: POST /v1/scheduling/proposals/:id/respond
API-->>WH: proposal.responded
Note over API: When every participant has responded, or the organizer calls resolve
API->>API: pickBestSlot() + create event
API-->>WH: proposal.confirmed
Note over API,WH: Alternate terminal states
API-->>WH: proposal.cancelled (organizer cancel or all_declined)
API-->>WH: proposal.expired (expires_at passed)

For a code-heavy walkthrough, see the multi-agent scheduling and negotiation guide.


POST /v1/scheduling/proposals
FieldTypeRequiredDescription
titlestringYesHuman-readable title (max 500 chars)
descriptionstringNoOptional details (max 5000 chars)
organizer_agent_idstringYesAgent ID of the proposal organizer
participant_agent_idsstring[]Yes1–50 participant agent IDs
calendar_idstringYesDefault target calendar for the resolved event
slotsobject[]Yes1–20 candidate slots (see below)
expires_atstringNoISO timestamp — proposal auto-expires if unresolved
metadataobjectNoArbitrary JSON, max 16 KB

Each slot is:

FieldTypeRequiredDescription
start_timestringYesISO datetime
end_timestringYesISO datetime
weightnumberNoOrganizer preference, 0–10 (default 1.0)
calendar_idstringNoOverride target calendar for this specific slot
Terminal window
curl -X POST https://api.chronary.ai/v1/scheduling/proposals \
-H "Authorization: Bearer chr_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Q2 planning sync",
"organizer_agent_id": "agt_organizer",
"participant_agent_ids": ["agt_alice", "agt_bob"],
"calendar_id": "cal_team",
"slots": [
{ "start_time": "2026-04-20T14:00:00Z", "end_time": "2026-04-20T15:00:00Z", "weight": 2.0 },
{ "start_time": "2026-04-21T14:00:00Z", "end_time": "2026-04-21T15:00:00Z" }
],
"expires_at": "2026-04-19T00:00:00Z"
}'

Returns the proposal summary (full object without nested slots/responses):

{
"id": "spr_a1b2c3",
"title": "Q2 planning sync",
"organizer_agent_id": "agt_organizer",
"participant_agent_ids": ["agt_alice", "agt_bob"],
"calendar_id": "cal_team",
"status": "pending",
"expires_at": "2026-04-19T00:00:00Z",
"metadata": {},
"created_at": "2026-04-16T12:00:00Z",
"updated_at": "2026-04-16T12:00:00Z"
}
StatusTypeCause
400validationInvalid slot times, empty participants, or malformed payload
404not_foundOrganizer or participant agent not found
429quota_exceededMonthly proposal quota reached

GET /v1/scheduling/proposals
ParameterTypeDefaultDescription
statusstringFilter by pending, confirmed, expired, or cancelled
organizer_agent_idstringFilter by organizer
limitinteger501–200
offsetinteger0Pagination offset

GET /v1/scheduling/proposals/:id

Returns the full proposal including slots and responses arrays.

StatusTypeCause
404not_foundProposal not found

POST /v1/scheduling/proposals/:id/respond
FieldTypeRequiredDescription
agent_idstringYesThe responding participant
responsestringYesaccept, decline, or counter
selected_slot_idstringIf acceptID of the slot the agent is accepting
counter_slotsobject[]NoUp to 20 alternative slots (informational when countering)
messagestringNoUp to 2000 chars

Each participant may respond at most once per proposal. Subsequent responses return 409 conflict with type duplicate_response.

Terminal window
curl -X POST https://api.chronary.ai/v1/scheduling/proposals/spr_a1b2c3/respond \
-H "Authorization: Bearer chr_sk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agt_alice",
"response": "accept",
"selected_slot_id": "slt_x7"
}'
StatusTypeCause
400validationMissing selected_slot_id when accepting
403forbiddenAgent is not a participant
404not_foundProposal or agent not found
409conflictProposal is not in pending state
409conflictAgent has already responded

POST /v1/scheduling/proposals/:id/resolve

Picks the winning slot using organizer preference weights plus participant acceptances, creates an event on the proposal’s calendar_id, and marks the proposal confirmed. If every participant has declined, the proposal is marked cancelled instead.

When confirmed:

{
"status": "confirmed",
"resolved_slot": {
"id": "slt_x7",
"start_time": "2026-04-20T14:00:00Z",
"end_time": "2026-04-20T15:00:00Z",
"weight": 2.0,
"calendar_id": null
}
}

When all participants declined:

{ "status": "cancelled", "reason": "all_declined" }
StatusTypeCause
404not_foundProposal not found
409conflictProposal is not in pending state

When the last pending response arrives via POST /respond, Chronary runs the same resolution logic as an explicit POST /resolve call — the organizer does not need to resolve manually. The scoring rule is additive: score = slot.weight + Σ(response_scores), with accept = 1.0, counter = 0.3, decline = 0.0. Ties break by earliest start_time. If every response is a decline, the proposal is cancelled with reason all_declined instead of confirmed.


POST /v1/scheduling/proposals/:id/cancel

Cancels a pending proposal. Cannot be undone.

{ "status": "cancelled" }
StatusTypeCause
404not_foundProposal not found
409conflictProposal is not in pending state

Subscribe to these webhook types to receive push notifications for proposal lifecycle transitions. See the webhooks guide for signature verification and retry behavior.

TypeFired whenPayload
proposal.createdPOST /v1/scheduling/proposals succeedsFull proposal summary
proposal.respondedA participant calls POST /:id/respondproposal_id, agent_id, response
proposal.confirmedResolution picks a winning slot (via auto-resolve or explicit POST /:id/resolve)proposal_id, resolved_slot, created_event_id
proposal.cancelledOrganizer calls POST /:id/cancel, or every response was declineproposal_id, reason (organizer_cancelled or all_declined)
proposal.expiredexpires_at passed without resolutionproposal_id