Graine AI

Scheduling

schedulingRules, scheduleCall, scheduledCalls, cancelScheduledCall and the SDK's booking sheet on every platform, React Native's useGraineScheduling, and the chat's upcoming calls — parameters, the checks made on the device first, what each returns and when, every error, and side effects.

How scheduled calls behave — who may book, when they ring, statuses — is on Scheduled calls. A call booked here rings the identified customer's app through hosted ringing; a call booked from a website rings their app too, and ends no_device when they have none.

schedulingRules

What the agent allows, read from its settings when the SDK loads them. A property: no request.

client.schedulingRules: GraineSchedulingRules

Returns GraineSchedulingRules. Before the agent's settings have loaded, and from an older server, it holds the defaults — usersCanBook: false, 5 minutes, 14 days, 3 waiting, 45 seconds.

{
  "usersCanBook": true, "minLeadMinutes": 5, "maxDaysAhead": 14, "maxPendingPerUser": 3,
  "ringTimeoutSeconds": 45, "callName": "Asha from Acme", "hostedRinging": true, "platforms": ["ios", "android"]
}

earliest(now) and latest(now) give the first and last times a booking may take. Android adds ringsAndroid, and iOS's hosted-calls settings ringsThisPlatform: whether Graine can ring this platform.

scheduleCall

Books a call for the customer.

client.scheduleCall(at: Date | string | number, reason?: string): Promise<GraineScheduledCall>
client.scheduleCall(request: { at: Date | string | number; reason?: string }): Promise<GraineScheduledCall>
 
const call = await client.scheduleCall(new Date(Date.now() + 60 * 60_000), "Talk through my order");

A string at is ISO-8601 with its offset; a number is milliseconds since the epoch.

ParameterTypeRequiredLimits
atA dateYesFrom earliest(now) to latest(now), inside the agent's calling windowSent with the device's offset and time zone
reasonStringNo120 charactersWhat the call is about. It reaches the agent when the call connects and never travels in the ring. Kotlin Multiplatform trims it to 120; Flutter refuses a longer one on the device with invalid_request

Checks on the device first, before any request, from schedulingRules: scheduling_off when usersCanBook is false, too_soon before earliest(now) (React Native allows 30 seconds' grace), too_far after latest(now).

Returns the booked GraineScheduledCall, status scheduled.

{
  "id": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35", "at": "2026-10-14T12:30:00Z", "status": "scheduled", "source": "app",
  "reason": "Talk through my order", "isUpcoming": true, "canCancel": true
}

Resolves when Graine has booked it.

Errors on the device: scheduling_off, too_soon, too_far (and Flutter's invalid_request). From Graine: outside_window, too_many_pending, device_limit, identity_required, invalid_request, rate_limited (at most 10 bookings an hour from one install), network, unavailable — Errors.

Side effects the call is booked for the identified customer when this device is one of theirs, otherwise for this install. React Native emits scheduled_calls_changed ({ booked: id }).

Threading Android calls done on the main thread.

scheduledCalls

The customer's — or this install's — upcoming calls.

client.scheduledCalls(): Promise<GraineScheduledCall[]>

Returns a list, upcoming first. Errors network, rate_limited, unavailable, invalid_key, origin_refused.

[
  { "id": "c3a8f1d2-5b7e-4a19-9f02-6d4e8b1a7c35", "at": "2026-10-14T12:30:00Z", "status": "scheduled", "source": "agent", "reason": "Follow up on the return", "isUpcoming": true, "canCancel": true },
  { "id": "f2d6a3c1-8e4b-4b7a-9c10-5d3e2a8b6f74", "at": "2026-10-16T09:00:00Z", "status": "scheduled", "source": "app", "reason": null, "isUpcoming": true, "canCancel": true }
]

cancelScheduledCall

client.cancelScheduledCall(id: string): Promise<GraineCallStatus>

Returns the call's status afterwards — cancelled. Errors not_found (not this customer's or install's, or no such call), network, rate_limited, unavailable. Only a call whose canCancel is true can be cancelled.

Side effects React Native emits scheduled_calls_changed ({ cancelled: id }).

useGraineScheduling

React Native's hook for a booking screen.

useGraineScheduling(): {
  rules: GraineSchedulingRules;
  calls: GraineScheduledCall[];
  schedule: (at: Date | string | number, reason?: string) => Promise<GraineScheduledCall>;
  cancel: (id: string) => Promise<GraineCallStatus>;
  refresh: () => Promise<void>;
  loading: boolean;
  error: GraineError | null;
  bookingAvailable: boolean;
  openBooking: (options?: { reason?: string }) => Promise<GraineScheduledCall | null>;
}
 
const { rules, calls, schedule, cancel, loading, error } = useGraineScheduling();

It loads the upcoming calls when it mounts and again after each booking or cancellation, and follows rules as the agent's settings load. bookingAvailable and openBooking are the SDK's booking sheet — see Booking a call.

Booking a call

The SDK's own booking sheet: the day, the time, what the call is about and Book call, drawn in your agent's design — "Call me at…" with no screen of your own. It resolves with the booked call when the sheet closes, or nothing when the customer closed it without booking.

client.bookingAvailable: boolean                  // also in useGraineAgent() and useGraineScheduling()
client.openBooking(options?: { reason?: string }): Promise<GraineScheduledCall | null>
 
<GraineBookingSheet visible={open} onClose={() => setOpen(false)} onBooked={(call) => {}} reason="Talk through my order" />

The provider draws the sheet openBooking asks for. The time is a grid of times every 15 minutes.

ParameterTypeRequiredLimits
reasonStringNo120 charactersFills "What's it about? (optional)"; the customer may change it

bookingAvailable is true only when all of these hold — the same rule on every platform:

  • the agent lets users book (schedulingRules.usersCanBook);
  • Graine can ring this phone: your workspace has In-app calling set up (hostedRinging) for this platform (platforms empty, or naming it). A website cannot be rung, so there this rule does not apply: the booking rings the customer's app;
  • the customer is identified with the trait your Who is the user? setting names. Without it a booking is refused with identity_required.

What the sheet offers. A chip per day, from today to the last day maxDaysAhead allows ("Today", "Tomorrow", then the weekday and date); times from earliest(now) rounded up to 5 minutes to latest(now) rounded down to 5 minutes, and no day that has no time left in it; the phone's time zone beside the time chosen.

Refusals stay in the sheet, as one line under Book call:

CodeThe line
too_soonPick a time at least 5 minutes from now (the agent's lead time). The sheet moves to the new earliest time
too_farPick a time within 14 days (the agent's limit)
outside_windowThat time is outside the hours we can call — pick another
too_many_pendingYou already have 3 calls booked — cancel one first (the agent's limit)
scheduling_offBooking a call isn't available right now
identity_requiredSign in to book a call
anything elseCouldn't book just now — try again

scheduleCall refuses scheduling_off, too_soon and too_far in the same words before anything is sent — the message of the error it throws — on every SDK and the website.

Opened where bookingAvailable is false, the sheet opens, shows the scheduling_off or identity_required line and does not book.

Once it is booked the sheet says "Booked — Acme will call you on Wed 14 Oct, 6:00 PM" with Done — always the date, never "Today". The name is the agent's call name, else the chat's title, else the heading.

The chat's upcoming calls

The SDKs' chat window lists the customer's upcoming calls with a cancel button and — where bookingAvailable is true and the design keeps the booked-calls card — Book a call, which opens the booking sheet: under the booked calls, or on its own where none are booked. The same list is yours to read:

PlatformListRefreshCancel
iOSchat.upcomingCalls: [GraineScheduledCall] (published)chat.refreshUpcomingCalls()chat.cancelUpcomingCall(id:)
Androidchat.upcomingCalls: List<GraineScheduledCall>chat.refreshUpcomingCalls()chat.cancelUpcomingCall(id)
Flutterchat.upcomingCallschat.refreshUpcoming()chat.cancelUpcoming(id), with chat.cancelStateOf(id) → none, cancelling or failed

These update the list in place and report failures in the list's own state rather than throwing.

Example: a booking screen

const rules = client.schedulingRules;
if (rules.usersCanBook) {
  const first = rules.earliest(new Date()), last = rules.latest(new Date());
  try {
    const call = await client.scheduleCall(picked, "Talk through my order");
    show(`We'll call you at ${call.at?.toLocaleString()}.`);
  } catch (e) {
    show(e instanceof GraineError ? e.message : "Couldn't book the call.");
  }
}

On this page