kintone integration

kintone has no scheduler. Keep cli-kintone, replace cron.

kintone triggers on user actions only, so every nightly sync, bulk job, and core-system integration needs something outside kintone to run it. Dagu is that something: it keeps the official cli-kintone tool and adds the retries, alerts, history, and cursors that cron never gave you.

Runs inside your network, reaching kintone and on-premises systems
Differential sync that respects the daily API request limit
Failure alerts, retries, and run history the moment cron is replaced
Free to self-host, with no per-connector licence
01

kintone triggers on people, not on time

Cybozu's own developer network states it plainly: kintone has no feature for running integration processing on a schedule, so you need a mechanism to run it. Their recommended answer is cli-kintone driven by cron on Linux, Task Scheduler on Windows, or AWS Lambda where neither exists. Customization JavaScript is not an option here because it only runs when somebody opens the page.

  • Nightly aggregation, month-end processing, and bulk updates all need an external runner.
  • cron gives you execution and nothing else: no retry, no alert, no history, no dependencies.
  • The failure mode everyone reports is the same one: the sync died and nobody noticed until a user complained.
02

Keep the official tool, replace the scheduler

cli-kintone already solves paging, filtering, tables, and attachments. Dagu does not replace it and has no kintone-specific executor. An existing shell script becomes a step, and everything cron was missing arrives with it.

  • retry_policy handles the transient API errors that make a cron job fail for the night.
  • mail_on.failure and handler_on.failure mean a dead sync pages someone instead of going quiet.
  • max_active_runs: 1 stops a slow run overlapping the next one, which is a classic source of duplicated records.
Nightly backup of an app and its attachments
# kintone-backup.yaml
schedule: "0 1 * * *"
max_active_runs: 1

env:
  - KINTONE_BASE_URL: https://example.cybozu.com
  - KINTONE_APP_ID: "42"

s3:
  bucket: corp-kintone-backup
  region: ap-northeast-1

steps:
  - id: export
    run: |
      cli-kintone record export \
        --base-url "$KINTONE_BASE_URL" \
        --app "$KINTONE_APP_ID" \
        --api-token "$KINTONE_API_TOKEN" \
        --attachments-dir ./attachments \
        > records.csv
    retry_policy:
      limit: 3
      interval_sec: 60

  - id: pack
    action: archive.create
    with:
      source: ./attachments
      destination: attachments.tar.gz
    depends: export

  - id: upload_records
    action: s3.upload
    with:
      key: kintone/app-42/records.csv
      source: records.csv
    depends: pack

  - id: upload_attachments
    action: s3.upload
    with:
      key: kintone/app-42/attachments.tar.gz
      source: attachments.tar.gz
    depends: pack

mail_on:
  failure: true
03

The API limit is why differential sync matters

A kintone app allows 10,000 API requests per day. Exceed it and Cybozu emails the store administrator the next morning; keep exceeding it and they may contact you and suspend API processing. A full reload every ten minutes burns that budget. Syncing only what changed since the last successful run does not.

  • Dagu stores a cursor that survives across runs, so each run asks kintone only for records updated since the last one.
  • save_cursor depends on the import step, so a failed load leaves the cursor where it was and the next run retries the same window.
  • Reads are capped at 500 records per request, and writes at 100, with offset paging limited to 10,000 records. Filtering by an updated-since window is what keeps you under all three.

The stored value arrives as a JSON envelope on the step's stdout, so the consuming command reads it through the .value key. Referencing the state step's output directly does not resolve.

Differential sync with a cursor that survives restarts
# kintone-incremental-sync.yaml
schedule: "*/30 * * * *"
max_active_runs: 1

env:
  - KINTONE_BASE_URL: https://example.cybozu.com
  - KINTONE_APP_ID: "42"

steps:
  - id: load_cursor
    action: state.get
    output: CURSOR
    with:
      key: cursors/kintone-app-42
      default:
        updated_since: "2026-01-01T00:00:00Z"

  - id: window_start
    run: |
      printf 'now=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$DAGU_OUTPUT_FILE"
    outputs:
      - name: now
    depends: load_cursor

  - id: export_changed
    run: |
      since="$(printf '%s\n' "$CURSOR" | jq -r .value.updated_since)"
      cli-kintone record export \
        --base-url "$KINTONE_BASE_URL" \
        --app "$KINTONE_APP_ID" \
        --api-token "$KINTONE_API_TOKEN" \
        --condition "更新日時 > \"$since\"" \
        > changed.csv
    depends: window_start
    retry_policy:
      limit: 3
      interval_sec: 60

  - id: load_to_core
    run: ./scripts/import-to-core.sh changed.csv
    depends: export_changed

  - id: save_cursor
    action: state.set
    with:
      key: cursors/kintone-app-42
      value:
        updated_since: "${steps.window_start.outputs.now}"
    depends: load_to_core

handler_on:
  failure:
    run: ./scripts/notify-sync-failure.sh

mail_on:
  failure: true
04

Why this polls instead of using webhooks

kintone webhooks look like the obvious trigger and are the wrong foundation for a sync. The webhook setting accepts a URL, an event list, and nothing else.

  • There is no field for request headers and no signature mechanism, so the receiving endpoint cannot be authenticated by the sender.
  • Notifications are capped at 60 per minute and anything beyond that is dropped without an error, so a bulk edit silently loses events.
  • CSV and Excel imports, bulk deletion, and bulk API operations do not fire webhooks at all, which means the operations that move the most data are exactly the ones an event-driven sync never sees.

Webhooks remain useful for cutting latency on single interactive records. They are a latency optimisation on top of a polling design, not a replacement for it.

05

Approved in kintone, executed by Dagu

kintone process management is better at approval than any workflow engine: 多段階承認, 代理承認, conditional routing, and a UI a non-engineer uses on a phone. Leave the approval there. Dagu picks up the records that reached 承認済 and does the work, then writes the result back to the record.

  • The people approving keep working in the tool they already know.
  • Dagu's step retries and failure handlers cover the execution half, which is where kintone stops.
  • Writing the outcome back to the record keeps the audit trail in one place.
Provision from approved records, then write back
# kintone-approved-provision.yaml
schedule: "*/10 * * * *"
max_active_runs: 1

env:
  - KINTONE_BASE_URL: https://example.cybozu.com
  - KINTONE_APP_ID: "77"
  - KINTONE_API_TOKEN: ${KINTONE_API_TOKEN}

steps:
  - id: fetch_approved
    action: http.request
    output: APPROVED
    with:
      method: GET
      url: ${KINTONE_BASE_URL}/k/v1/records.json
      headers:
        X-Cybozu-API-Token: ${env.KINTONE_API_TOKEN}
      query:
        app: ${KINTONE_APP_ID}
        query: 'ステータス in ("承認済") and 処理状況 in ("未処理") limit 100'
      silent: true

  - id: provision
    run: ./scripts/provision-accounts.sh "$APPROVED"
    depends: fetch_approved
    retry_policy:
      limit: 2
      interval_sec: 120

  - id: write_back
    run: ./scripts/mark-records-done.sh
    depends: provision

handler_on:
  failure:
    run: ./scripts/notify-provision-failure.sh

mail_on:
  failure: true
06

Reaching the systems kintone cannot

The usual blockers for connecting kintone to an on-premises ERP or production system are security and client licence cost. A single binary running inside your own network sidesteps both: it makes outbound HTTPS calls to kintone and local calls to everything else, with no inbound firewall change.

  • Drive on-premises hosts over SSH without installing a resident agent.
  • Secrets are declared at workflow level and redacted in run logs rather than passed through ambient environment variables.
  • Steps run with a controlled environment, so anything a script needs has to be declared instead of leaking in from the host.

The sql step supports PostgreSQL and SQLite. An Oracle or SQL Server core system is driven through its own client, such as sqlplus or sqlcmd, run as a command step.

07

When a commercial tool is the better answer

Dagu is a scheduler you operate. That is the right trade for some teams and the wrong one for others.

  • If nobody on the team will own a server, a hosted integration product is the honest choice.
  • If the people building integrations are not engineers, a GUI-driven tool such as krewData or an EAI product will be faster to adopt than YAML.
  • If you need a vendor support contract with a local-language SLA, Dagu offers a community Discord and email support on paid plans, which is not the same thing.

FAQ

Practical questions before adopting

Is there a kintone executor or plugin?

No. Dagu drives kintone through the official cli-kintone command or plain HTTP steps against the REST API with an X-Cybozu-API-Token header. That is deliberate: cli-kintone already handles paging, attachments, and table fields, and reimplementing it would only add a layer to keep in sync with Cybozu's releases.

How do I stay under the API request limit?

Sync differentially rather than reloading. Store the timestamp of the last successful run, ask kintone only for records updated since then, and read in pages of 500. Also widen the schedule interval: a job every thirty minutes uses a sixth of the requests of one every five minutes, and for most core-system syncs nobody can tell the difference.

Can kintone call Dagu directly when a record changes?

Not securely. A kintone webhook sends only a URL and a payload, with no request headers and no signature, while Dagu's webhook trigger expects a bearer token. Reaching a Dagu instance inside the network would also need a tunnel. Polling on a schedule avoids both problems and, because CSV imports and bulk operations do not fire webhooks at all, it is the more correct design regardless.

What happens if a sync run fails halfway?

The cursor only advances after the import step succeeds, so a failed run leaves it untouched and the next run retries the same window. Combined with max_active_runs set to one, that means a failure delays data rather than losing or duplicating it.

Does this work with Oracle or SQL Server on the core system side?

Yes, through their own clients. The built-in sql step covers PostgreSQL and SQLite, so an Oracle or SQL Server system is driven by running sqlplus or sqlcmd as an ordinary command step, locally or over SSH. Everything else on the page applies unchanged.

Next step

Start with one workflow.

Install Dagu, move one script that runs on cron today into YAML, and decide from a real run history.