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

| Field | Type | Required | Description | |-------|------|----------|-------------| | title | string | Yes | Human-readable title (max 500 chars) | | description | string | No | Optional details (max 5000 chars) | | organizer_agent_id | string | Yes | Agent ID of the proposal organizer | | participant_agent_ids | string[] | Yes | 1–50 participant agent IDs | | calendar_id | string | Yes | Default target calendar for the resolved event | | slots | object[] | Yes | 1–20 candidate slots (see below) | | expires_at | string | No | ISO timestamp — proposal auto-expires if unresolved | | metadata | object | No | Arbitrary JSON, max 16 KB |

Each slot is:

| Field | Type | Required | Description | |-------|------|----------|-------------| | start_time | string | Yes | ISO datetime | | end_time | string | Yes | ISO datetime | | weight | number | No | Organizer preference, 0–10 (default 1.0) | | calendar_id | string | No | Override 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"
}

| Status | Type | Cause | |--------|------|-------| | 400 | validation | Invalid slot times, empty participants, or malformed payload | | 404 | not_found | Organizer or participant agent not found | | 429 | quota_exceeded | Monthly proposal quota reached |


GET /v1/scheduling/proposals

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | status | string | — | Filter by pending, confirmed, expired, or cancelled | | organizer_agent_id | string | — | Filter by organizer | | limit | integer | 50 | 1–200 | | offset | integer | 0 | Pagination offset |


GET /v1/scheduling/proposals/:id

Returns the full proposal including slots and responses arrays.

| Status | Type | Cause | |--------|------|-------| | 404 | not_found | Proposal not found |


POST /v1/scheduling/proposals/:id/respond

| Field | Type | Required | Description | |-------|------|----------|-------------| | agent_id | string | Yes | The responding participant | | response | string | Yes | accept, decline, or counter | | selected_slot_id | string | If accept | ID of the slot the agent is accepting | | counter_slots | object[] | No | Up to 20 alternative slots (informational when countering) | | message | string | No | Up 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"
}'

| Status | Type | Cause | |--------|------|-------| | 400 | validation | Missing selected_slot_id when accepting | | 403 | forbidden | Agent is not a participant | | 404 | not_found | Proposal or agent not found | | 409 | conflict | Proposal is not in pending state | | 409 | conflict | Agent 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" }

| Status | Type | Cause | |--------|------|-------| | 404 | not_found | Proposal not found | | 409 | conflict | Proposal 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" }

| Status | Type | Cause | |--------|------|-------| | 404 | not_found | Proposal not found | | 409 | conflict | Proposal 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.

| Type | Fired when | Payload | |------|-----------|---------| | proposal.created | POST /v1/scheduling/proposals succeeds | Full proposal summary | | proposal.responded | A participant calls POST /:id/respond | proposal_id, agent_id, response | | proposal.confirmed | Resolution picks a winning slot (via auto-resolve or explicit POST /:id/resolve) | proposal_id, resolved_slot, created_event_id | | proposal.cancelled | Organizer calls POST /:id/cancel, or every response was decline | proposal_id, reason (organizer_cancelled or all_declined) | | proposal.expired | expires_at passed without resolution | proposal_id |