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 responsesconfirmed— resolved; a winning slot was picked and an event createdexpired— TTL passed without resolutioncancelled— cancelled by the organizer or auto-cancelled because all participants declined
Lifecycle at a glance
Section titled “Lifecycle at a glance”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.
Create a proposal
Section titled “Create a proposal”POST /v1/scheduling/proposalsRequest body
Section titled “Request body”| 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 |
Example
Section titled “Example”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" }'from chronary import Chronary
client = Chronary()
proposal = client.scheduling.create( 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",)print(proposal.id) # "spr_..."import Chronary from '@chronary/sdk';
const client = new Chronary();
const proposal = await client.scheduling.create({ 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',});Response 201 Created
Section titled “Response 201 Created”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"}Errors
Section titled “Errors”| 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 |
List proposals
Section titled “List proposals”GET /v1/scheduling/proposalsQuery parameters
Section titled “Query parameters”| 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 a proposal
Section titled “Get a proposal”GET /v1/scheduling/proposals/:idReturns the full proposal including slots and responses arrays.
Errors
Section titled “Errors”| Status | Type | Cause |
|--------|------|-------|
| 404 | not_found | Proposal not found |
Respond to a proposal
Section titled “Respond to a proposal”POST /v1/scheduling/proposals/:id/respondRequest body
Section titled “Request body”| 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.
Example
Section titled “Example”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" }'client.scheduling.respond( "spr_a1b2c3", agent_id="agt_alice", response="accept", selected_slot_id="slt_x7",)await client.scheduling.respond('spr_a1b2c3', { agent_id: 'agt_alice', response: 'accept', selected_slot_id: 'slt_x7',});Errors
Section titled “Errors”| 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 |
Resolve a proposal
Section titled “Resolve a proposal”POST /v1/scheduling/proposals/:id/resolvePicks 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.
Response 200 OK
Section titled “Response 200 OK”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" }Errors
Section titled “Errors”| Status | Type | Cause |
|--------|------|-------|
| 404 | not_found | Proposal not found |
| 409 | conflict | Proposal is not in pending state |
Auto-resolution
Section titled “Auto-resolution”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.
Cancel a proposal
Section titled “Cancel a proposal”POST /v1/scheduling/proposals/:id/cancelCancels a pending proposal. Cannot be undone.
Response 200 OK
Section titled “Response 200 OK”{ "status": "cancelled" }Errors
Section titled “Errors”| Status | Type | Cause |
|--------|------|-------|
| 404 | not_found | Proposal not found |
| 409 | conflict | Proposal is not in pending state |
Webhook events
Section titled “Webhook events”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 |