Samaya
Pre-beta. Samaya is early software: expect rough edges and changes between releases. Keep a backup of your Org files.

Google Calendar

Samaya syncs your Google calendars into calendar.org in your Org folder. The app syncs while it is open; samaya sync run keeps the file fresh for org-agenda while it is closed.

Connecting an account

In the app, open Settings → Calendars → + Connect calendar. Give the account a name, then sign in with Google in the browser that opens.

An account connected this way syncs every calendar it has, including ones you add or are shared later. Each calendar has a switch that hides it from Samaya's views (it still syncs into calendar.org) and a colour, starting from its colour in Google. The account's card re-authorizes and disconnects it. Sync all calendars does the same for an account connected from the terminal.

From a terminal:

samaya sync auth google

It asks for an account name, the Org tag its events should carry and, the first time, your Org folder. It then opens your browser, and once you have signed in, lists the account's calendars so you can choose which to sync. Every prompt has a flag:

samaya sync auth google \
  --account work --tag work \
  --calendar primary --calendar team@example.com \
  --no-interactive

Samaya asks to read your calendars and to edit events (calendar.readonly and calendar.events), so it can create task events, change event lengths and answer invitations.

Your own OAuth client (optional)

Connecting needs no setup: Samaya signs in through its own OAuth client. Use your own instead to have your own API quota, or if your organization only allows its own apps. In the connect form, choose Sign in through → Your own app.

  1. Create a project at https://console.cloud.google.com/projectcreate.

  2. Enable the Google Calendar API for it.

  3. Configure the OAuth consent screen with the External user type.

  4. Set the publishing status to "In production". A client left in Testing issues sign-ins that expire after seven days, so every account would need re-authorizing each week. Publishing without verification costs one "Google hasn't verified this app" screen, clicked through once per account.

  5. Create an OAuth client ID of type Desktop app. Keep the client ID and client secret to hand.

From a terminal, pass them to the first account you connect. Later accounts reuse its client.

samaya sync auth google --account work \
  --client-id ....apps.googleusercontent.com --client-secret ...

On Google Workspace, a client with user type Internal avoids the warning screen, but can only sign in accounts in that organization. One External client covering work and personal accounts is usually simpler.

Scheduling tasks into a calendar

Choose a calendar in Settings → Calendars → New task events. Dropping a task onto a time slot then creates an event in that calendar with the task's title, notes and duration. The block keeps the task's channel colour.

  • Moving or stretching the block updates its event.

  • Changing New task events affects newly scheduled tasks only.

  • Local tasks only keeps new time blocks in the planner.

  • Removing a task's time block leaves its calendar event in place.

Failed writes retry on later syncs, including after a restart. Settings → Calendars shows their progress, or a retry action. Pausing sync pauses them too.

Answering invitations

An event's card, opened on hover or click, has a Going? row with Yes, No and Maybe for any upcoming invitation someone else organised. A reply changes only your own response, for that occurrence only, and Google tells the organiser. calendar.org picks up the new state straight after.

An account connected before replies were supported has read access only. Re-authorize it once from Settings → Calendars, or with samaya sync reauth, and accept the new permission.

Re-authorizing

When an account's sign-in expires, the tray reports that it needs re-authorization. Re-authorize it from its card in Settings → Calendars, or from a terminal:

samaya sync reauth --account personal

This signs in again and changes nothing else. Sign in as the same Google account; a different one is refused. A running samaya sync run --watch needs no restart.

Changing an account's calendars or tag

samaya sync auth refuses a name that is already connected. To change an account on purpose:

samaya sync auth google --account personal --reconfigure

Every prompt defaults to what the account already has. The calendar picker marks the calendars currently synced, and a blank answer keeps them.

Syncing from the terminal

samaya sync run --once     # sync once, then exit
samaya sync run --watch    # sync now, then every interval
samaya sync status         # accounts, calendars, events and last sync
samaya sync reauth --account work
samaya sync disconnect --account work

While the app is open it does the syncing, and samaya sync run refuses with an error. status, auth, reauth and disconnect always work.

To sync while the app is closed, use either a systemd user service with --watch, as in Getting started, or a timer with --once. Enable one, not both.

For a timer, ~/.config/systemd/user/samaya-sync.service:

[Unit]
Description=Samaya calendar synchronization
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=%h/.local/bin/samaya sync run --once

and ~/.config/systemd/user/samaya-sync.timer:

[Unit]
Description=Run Samaya calendar synchronization periodically

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Persistent=true

[Install]
WantedBy=timers.target
systemctl --user enable --now samaya-sync.timer

While the app is open, the timer's runs are refused; they resume once it closes.

Configuration

~/.config/samaya/sync.toml holds no secrets:

[sync]
org_directory = "/home/you/org"   # the folder the planner uses
interval_minutes = 5

[sync.window]
past_days = 30
future_days = 180

[google.work]
calendars = ["primary", "team@example.com"]
tag = "work"
  • interval_minutes is how often --watch syncs.

  • window is a rolling span of days around today. A wider future_days makes a longer file: a daily standup over 180 days is about 130 entries.

  • calendars lists the calendars to sync; all_calendars = true syncs every calendar on the account instead.

  • tag is the Org tag on the account's events; it defaults to the account's name.

Samaya writes the other fields of an account's table itself. Leave them as they are.

Credentials

Client secrets and sign-ins go to the system credential store: Secret Service on Linux, Credential Manager on Windows. Where none is reachable, as on most servers, they are kept in files under ~/.local/share/samaya/credentials/, readable only by you.

A locked keyring is reported as such, and background syncs never prompt. If a --watch service starts before your session unlocks the keyring, its first sync fails and the next one succeeds.

The calendar file

calendar.org is generated, and every sync rewrites it. Don't edit the generated parts of an event: its headline, planning line, :PROPERTIES: and :DETAILS:. Notes you write beneath an event are kept.

#+TITLE: Calendar
#+TODO: MEETING ACCEPTED TENTATIVE EVENT | ATTENDED DECLINED CANCELLED

* MEETING Design review                                          :work:
DEADLINE: <2026-08-10 Mon 14:00-15:00>
:PROPERTIES:
:ID: gcal-work-...
:CALENDAR: primary
:CONFERENCE: https://meet.google.com/...
:LOCATION: Meeting room 2
:URL: https://calendar.example.com/event?eid=...
:END:
:DETAILS:
Attendees:
- Alice Smith <alice@example.com> :: organiser, accepted
- you@example.com :: you, awaiting
Description:
  Quarterly roadmap review.
:END:

Bring the roadmap doc.

* EVENT Dentist                                              :personal:
DEADLINE: <2026-08-11 Tue 11:00-12:00>
:PROPERTIES:
:ID: gcal-personal-...
:CALENDAR: primary
:LOCATION: 12 High Street
:END:

"Bring the roadmap doc." is a note: it stays beneath its event across syncs.

KeywordMeaning
MEETINGAn invitation you have not answered: the only one that is a task
ACCEPTEDAn invitation you accepted, still to come
TENTATIVEAn invitation you answered Maybe, still to come
EVENTAn event that asked nothing of you: your own entry, a birthday
ATTENDEDAn event that has ended
DECLINEDAn invitation you declined
CANCELLEDAn event that was called off
  • The done keywords keep past and declined events out of org-agenda's task views, and a TODO match on MEETING lists the replies you owe.

  • Times are on DEADLINE:. All-day events have a bare date; an event over several days records its last day in :EVENT_END:.

  • :CONFERENCE: holds the meeting link, so org-open-at-point opens it.

  • Each event is tagged with its account's tag, for filtering in org-agenda.

  • :DETAILS: lists the guests, with you, organiser and each response (accepted, declined, maybe or awaiting), then the description.

  • Recurring events appear as one entry per occurrence.

archive/calendar.org

When an event with a note goes away, because it was deleted, aged out of the window, or its calendar or account was removed, its entry moves to archive/calendar.org with the note. Events without notes are simply dropped.

Samaya only ever adds to this file, so notes there are safe to edit. It is tagged :ARCHIVE:, so org-agenda leaves it out.

If an account can't be reached, its events and notes stay in calendar.org until it can.

Troubleshooting

samaya sync status names any account that needs signing in again; samaya sync reauth --account <name> fixes it. Other accounts keep syncing meanwhile, and the account's last known events stay in the file.

If the browser shows "Something went wrong" (accounts.google.com/info/unknownerror), the browser's Google session is usually confused by several signed-in accounts or a stale cookie. Copy the address the command prints into a private window; the command keeps waiting for it.

What Google saysWhat it means
invalid_client / "OAuth client was not found"Wrong client ID, or a client created moments ago that is not ready yet
redirect_uri_mismatchThe client is a Web application; it needs to be a Desktop app
"Access blocked: … has not completed the Google verification process"The consent screen is in Testing and this account is not a test user. Publish it, or add the account
"Something went wrong" with no codeThe browser session, as above, or a project whose consent screen was never configured