Copilotとkintoneを連携したAIエージェントを構築しよう

著者名:Aiko Hanatsuka( サイボウズ株式会社 (External link) )

目次

はじめに

本記事では、 kintone OpenAPI Spec を使って、Copilot Studioでカスタムエージェントを構築する手順例を紹介します。
カスタムエージェントを構築すると、Microsoft CopilotのAIエージェントが自社のkintoneデータをもとに回答やタスクを実行できるようになり、ビジネスの現場でAIとデータをより活用しやすくなります。

Copilot Studioについて

Microsoftでは 責任あるAI (External link) を採用しており、AIを安心してビジネスの中で活用できるツールやサービスが開発されています。
Copilot Studio (External link) は、Microsoftの高度なセキュリティ要件を保った状態で、社内データを扱う自社独自のAIエージェントをノーコード/ローコードで作れるサービスです。
そのため、外部に漏れてはいけない社内情報を使った回答をAIに生成させたり、自社専用のカスタムエージェントを社内に公開したりすることも安心して行えます。

そして、Copilot Studioでは、外部のシステムやAPIをAIエージェント自身に「ツール」として登録できます。
本記事でも、このしくみを使ってkintoneをツールとしてAIエージェントに追加します。

kintoneとの連携・認証のしくみ

AIエージェントとkintoneの連携を設計する際は、まず次の3つの観点で要件を整理しましょう。

  • 利用できるユーザー
  • 利用できるアプリ
  • 利用できる操作

連携できても、これらが過剰な権限(スコープ)設定になっていると、意図せずデータをAIに読み込まれたり書き換えられたりする恐れがあるためです。
特に「利用できるユーザー」の扱いは、kintoneとの認証方式によって変わるので、あらかじめ押さえておきましょう。

APIトークン認証とOAuthクライアント認証

kintoneとCopilotの認証方式には、主に次の2つがあります。
ユーザーをcybozu.com側で厳密に絞り込みたい場合はOAuth、まずは手軽に構築したい場合はAPIトークンが向いています。
OAuthクライアントを利用する場合は、次のページを参照してください。
OAuthクライアントを追加する

APIトークン認証 OAuthクライアント認証
発行単位 アプリごと cybozu.com全体で1つ
権限の主体 トークンに紐づく固定の権限 ログインした個々のユーザーの権限で動作
認証フロー リクエストヘッダーにトークンを付与 cybozu.comではAuthorization Code Grant(認可コードグラント)を利用
利用できるユーザー kintoneアプリ管理者権限をもつユーザー cybozu.com共通管理権限をもつユーザー
設定の手間 少ない やや多い

本記事では、より手軽なAPIトークン認証を使って手順を紹介します。
今回の、APIトークン認証での連携の全体像は、次のとおりです。

  1. kintoneでできる操作を定義したOASを読み込ませて、「 カスタムコネクタ (External link) 」を作成する。
  2. 作成したカスタムコネクタを、Copilot Studioで作成するエージェントに「ツール」として設定。このとき、kintoneアプリのAPIトークンを「コネクション」として設定する。
  3. 完成したエージェントをMicrosoft 365 Copilotなど組織内に公開し、業務で利用する。

ポイントは、カスタムコネクタを1つ作成するだけでよいことです。
カスタムコネクタは複数のアプリ・エージェントで再利用できるため、新しく連携したいアプリが増えても、そのアプリ用のAPIトークンでコネクションを設定するだけで、同じカスタムコネクタをツールとして使い回せます。

tips
補足

カスタムコネクタを作らずに、 HTTP要求ノード (External link) を使って連携させることもできます。
1つのエージェントで単発的に連携する場合はHTTP要求ノードでも十分ですが、次のような場合は本記事のようにOpenAPI Specからカスタムコネクタを作成しておくと管理しやすくなります。

  • 複数のエージェントで同じ操作を使い回したい場合
  • 認証情報を管理しやすくしたい場合
  • Power Automateなど他のMicrosoft Power Platform製品でも使いたい場合

完成イメージ

カスタムエージェントの完成イメージは、次のとおりです。
Microsoft 365のCopilotに、kintoneと連携するAIエージェント「営業アシスタント」が追加されます。

今回は、AIができる操作を、シンプルにレコードの取得、登録、更新に絞ったエージェントを作成します。
このように、Copilotからアクセスできるアプリ、APIを制限できるので、「このアプリだけを、限られた権限の範囲でAIに使わせたい」 といった要件を実現できます。

事前に必要なもの

本記事の設定手順の再現に必要なものは次のとおりです。

  • ユーザーは、Microsoft 365 Copilotを使用できるライセンスを持っていること
  • エージェント作成者は、Copilot Studio環境にアクセスし、エージェントを作成できる権限を持っていること

STEP1:kintoneアプリ側の準備

はじめに、Copilotと連携したいアプリを検討します。
アプリの設定画面で、APIトークンを発行してください。
APIトークンの生成方法は、次のページを参照してください。
APIトークンを生成する (External link)

本記事では、例としてサンプルアプリ SFA(営業支援)パック (External link) を使用します。
次の表のとおり、「レコード閲覧」、「レコード追加」、「レコード編集」のアクセス権にチェックを入れて作成します。

アプリ レコード閲覧 レコード追加 レコード編集
顧客管理アプリ ○ ○ -
活動履歴アプリ ○ ○ ○
案件管理アプリ ○ ○ -

APIトークンは、あとで使うのでコピーして控えておきます。

STEP2:OpenAPI Spec(OAS)の用意

次に、OpenAPI Specを準備します。
公式で提供している kintone OpenAPI Spec は、OpenAPI3.0です。
一方、Copilotでは、OpenAPI2.0のみサポートされているため、2.0に対応したOASのJSONまたはYAMLファイルを用意する必要があります。

今回、Copilotが扱えるkintone REST APIは次のとおりです。

フィールドを取得する APIは、各アプリにどんなフィールドがあるかをエージェント自身で取得して、理解するのに役立ちます。
これらのAPIを定義したOAS2.0のJSONファイルのサンプルsample.jsonを用意しました。
sample.json全体をコピーして保存したあと、"host": "sample.cybozu.com"の部分をご自身のサブドメインに書き換えてください。

sample.json
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
{
  "swagger": "2.0",
  "info": {
    "title": "kintone REST API (Copilot Studio validation profile)",
    "version": "1.0.0",
    "description": "kintone REST APIの一部をSwagger 2.0形式に変換したもの(Power Platform / Copilot StudioはOpenAPI 3.xに未対応のため)。\n\n次の4操作を提供します。\n\n1. `GET /app/form/fields.json` — フィールドコードと種類を取得\n2. `GET /records.json` — レコードを検索\n3. `POST /records.json` — レコードを一括登録(最大100件)\n4. `PUT /records.json` — レコードを一括更新(最大100件)\n\nkintone REST API全体(OpenAPI 3.0、204操作)を簡略化したサブセットです。利用前にhostを自身のkintoneサブドメインに変更してください。",
    "license": {
      "name": "MIT No Attribution",
      "url": "https://opensource.org/license/MIT-0"
    }
  },
  "host": "sample.cybozu.com",
  "basePath": "/k/v1",
  "schemes": [
    "https"
  ],
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "securityDefinitions": {
    "api_key": {
      "type": "apiKey",
      "in": "header",
      "name": "X-Cybozu-API-Token"
    }
  },
  "security": [
    {
      "api_key": []
    }
  ],
  "paths": {
    "/records.json": {
      "get": {
        "operationId": "getRecords",
        "summary": "Get records",
        "description": "Retrieves records from an App. Use the `query` parameter to filter, sort and page. Returns at most 500 records per call.",
        "parameters": [
          {
            "name": "app",
            "in": "query",
            "required": true,
            "description": "The App ID. This is the number in the App's URL, for example `31` in `https://example.cybozu.com/k/31/`.",
            "type": "integer"
          },
          {
            "name": "query",
            "in": "query",
            "required": false,
            "description": "Search conditions written in kintone's own query syntax. This is **not** SQL and **not** OData.\nOmit this parameter to retrieve all records the credential can access.\n\n## Syntax rules\n\n- The field code always goes on the **left** of the operator, never on the right.\n- Wrap string values in double quotes: `Status in (\"Closed\")`. Numbers and times are unquoted: `price > 10`, `Time > 10:00`.\n- Combine conditions with `and` / `or`, and group them with parentheses.\n- Field codes are defined per App and are case-sensitive. Call `GET /app/form/fields.json` first to obtain the exact field codes and their types. Do not guess field codes.\n\n## Operators allowed per field type\n\nUsing an operator that the field type does not support returns an error, so check the field type first.\n\n| Field type | Allowed operators |\n|---|---|\n| Single-line text, Link | `=` `!=` `in` `not in` `like` `not like` |\n| Multi-line text | `like` `not like` `is empty` `is not empty` — **`=` is not allowed** |\n| Rich text | `like` `not like` |\n| Number, Calculated | `=` `!=` `>` `<` `>=` `<=` `in` `not in` |\n| Drop-down, Radio button, Check box, Multi-choice | **`in` / `not in` only — `=` is not allowed** |\n| Date, Date and time, Time | `=` `!=` `>` `<` `>=` `<=` |\n| Created datetime, Updated datetime | `=` `!=` `>` `<` `>=` `<=` |\n| User selection, Department selection, Group selection | `in` `not in` |\n| Created by, Updated by | `in` `not in` |\n| Status | `=` `!=` `in` `not in` |\n| Record number, `$id` | `=` `!=` `>` `<` `>=` `<=` `in` `not in` |\n| Attachment | `like` `not like` `is empty` `is not empty` |\n| Lookup | Same as the field type it looks up |\n| Related Records, fields inside a Table | Same as the source field type, but `=` and `!=` are **not** allowed — use `in` / `not in` |\n| Field group, Category | Cannot be used in a query |\n\nTo reference a field inside a Related Records field, use `<relatedRecordsFieldCode>.<fieldCode>`.\n\n## Functions\n\n| Function | Applies to |\n|---|---|\n| `LOGINUSER()` | User selection, Created by, Updated by |\n| `PRIMARY_ORGANIZATION()` | Department selection |\n| `NOW()` `TODAY()` `YESTERDAY()` `TOMORROW()` | Date, Date and time, Created datetime, Updated datetime |\n| `FROM_TODAY(n, \"DAYS\"\\|\"WEEKS\"\\|\"MONTHS\"\\|\"YEARS\")` | same as above |\n| `THIS_WEEK(SUNDAY..SATURDAY)` `LAST_WEEK()` `NEXT_WEEK()` | same as above |\n| `THIS_MONTH(1..31\\|LAST)` `LAST_MONTH()` `NEXT_MONTH()` | same as above |\n| `THIS_YEAR()` `LAST_YEAR()` `NEXT_YEAR()` | same as above |\n\nArguments may be omitted to mean the whole week / month / year, e.g. `THIS_MONTH()`.\n\n## Options\n\nPlace these **after all conditions, in this exact order**: `order by`, `limit`, `offset`.\n\n| Option | Meaning |\n|---|---|\n| `order by <fieldCode> asc\\|desc` | Sort order. Separate multiple fields with commas. Defaults to record ID descending. |\n| `limit <n>` | Number of records to return. Default 100, **maximum 500**. |\n| `offset <n>` | Number of records to skip. **Maximum 10000.** To read past 10000 records, use the cursor API instead. |\n\n## Escaping\n\nInside double quotes, escape `\"` and `\\` with a backslash. Required for text, rich text, check box, radio button, drop-down, multi-choice and status fields.\nExample: `Checkbox in (\"sample\\\"1\\\"\")`\n\n## Examples\n\n```\nText like \"sample\" and Text_area like \"sample\" order by Created_datetime desc\nDrop_down in (\"value1\", \"value2\") and (Multi_choice in (\"value3\") or Radio_button in (\"value4\"))\nTime > 10:00 and Time < 19:00 and Created_datetime = TODAY() order by $id asc limit 10\nUser_selection in (LOGINUSER()) and Department_selection not in (PRIMARY_ORGANIZATION())\nCompany_DB.company_name in (\"kintone\") and Company_DB.location like \"San Francisco\"\nText_area is empty\n```",
            "type": "string"
          },
          {
            "name": "totalCount",
            "in": "query",
            "required": false,
            "description": "If set to `true`, the total count of records that match the query conditions will be included in the response.\nIgnoring this parameter will return `null`.",
            "type": "string"
          }
        ],
        "responses": {
          "200": {
            "description": "The matching records.",
            "schema": {
              "$ref": "#/definitions/GetRecordsResponse"
            },
            "examples": {
              "application/json": {
                "records": [
                  {
                    "$id": {
                      "type": "__ID__",
                      "value": "12"
                    },
                    "会社名": {
                      "type": "SINGLE_LINE_TEXT",
                      "value": "サイボウズ株式会社"
                    },
                    "売上": {
                      "type": "NUMBER",
                      "value": "1200000"
                    },
                    "商談フェーズ": {
                      "type": "DROP_DOWN",
                      "value": "提案"
                    },
                    "対応種別": {
                      "type": "CHECK_BOX",
                      "value": [
                        "訪問",
                        "電話"
                      ]
                    },
                    "受注予定日": {
                      "type": "DATE",
                      "value": "2026-08-31"
                    },
                    "更新者": {
                      "type": "MODIFIER",
                      "value": {
                        "code": "sean",
                        "name": "Sean"
                      }
                    }
                  }
                ],
                "totalCount": "1"
              }
            }
          },
          "400": {
            "description": "The request was rejected.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "401": {
            "description": "Unauthorized.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "403": {
            "description": "Forbidden.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "404": {
            "description": "NotFound.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "429": {
            "description": "Too many requests.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "495": {
            "description": "Cert Error.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "496": {
            "description": "No Cert.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "503": {
            "description": "Service Unavailable.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "default": {
            "description": "An unexpected error occurred.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          }
        }
      },
      "post": {
        "operationId": "addRecords",
        "summary": "Register (add) multiple records in an App",
        "description": "Registers up to 100 new records in a single call. Call `GET /app/form/fields.json` first so that field codes, required fields and choice strings (for selection fields) are known before building the request.\n\nRecords are registered in the order given in `records`. If any record in the request fails validation, **the entire batch is cancelled** and no records are registered.\n\nThe following fields cannot be set through this API: fields copied from a Lookup's source, Status, Category, Calculated, Assignee, and an auto-calculated Single-line text field. Setting Created by / Updated by / Created datetime / Updated datetime requires App Management permission on the credential; otherwise these are set automatically.\n\nRequired permissions: record-add permission on the App, and edit permission on every field being set.",
        "consumes": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "body",
            "in": "body",
            "required": true,
            "description": "The records to register.",
            "schema": {
              "$ref": "#/definitions/AddRecordsRequestBody"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The records were registered. `ids` and `revisions` are in the same order as the `records` given in the request.",
            "schema": {
              "$ref": "#/definitions/AddRecordsResponse"
            },
            "examples": {
              "application/json": {
                "ids": [
                  "100",
                  "101"
                ],
                "revisions": [
                  "1",
                  "1"
                ]
              }
            }
          },
          "400": {
            "description": "The request was rejected.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "401": {
            "description": "Unauthorized.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "403": {
            "description": "Forbidden.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "404": {
            "description": "NotFound.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "429": {
            "description": "Too many requests.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "495": {
            "description": "Cert Error.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "496": {
            "description": "No Cert.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "503": {
            "description": "Service Unavailable.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "default": {
            "description": "An unexpected error occurred.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          }
        }
      },
      "put": {
        "operationId": "updateRecords",
        "summary": "Update multiple records in an App",
        "description": "Updates up to 100 existing records in a single call. Each entry in `records` identifies one record either by `id` (the record ID) or by `updateKey` (a field/value pair on a field with duplicates disallowed) — never both. Call `GET /app/form/fields.json` first so that field codes, and which field is safe to use as an `updateKey`, are known.\n\nIf `upsert` is `true`, an entry whose target record does not already exist is registered as a new record instead of failing (UPSERT mode); this also requires record-add permission on the App. `updateKey` is required to use UPSERT mode, since a not-yet-existing record has no `id` to match against.\n\nIf `records[].record` is omitted for an entry, that record's field values are left unchanged. If `records[].revision` is given, the update is rejected (and the whole batch cancelled) when it does not match the record's current revision; omit it, or pass `-1`, to skip this check.\n\nIf any entry in the request fails, **the entire batch is cancelled** and no records are updated.\n\nThe following fields cannot be updated through this API: fields copied from a Lookup's source, Status, Category, Calculated, Assignee, Created by, Created datetime, Updated by, Updated datetime, and an auto-calculated Single-line text field.\n\nRequired permissions: record-edit permission on the App, and edit permission on the records and fields being updated (plus record-add permission on the App when `upsert` is `true`).",
        "consumes": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "body",
            "in": "body",
            "required": true,
            "description": "The records to update.",
            "schema": {
              "$ref": "#/definitions/UpdateRecordsRequestBody"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The records were updated (or, in UPSERT mode, registered). `records` is in the same order as the `records` given in the request.",
            "schema": {
              "$ref": "#/definitions/UpdateRecordsResponse"
            },
            "examples": {
              "application/json": {
                "records": [
                  {
                    "id": "1",
                    "revision": "5",
                    "operation": "UPDATE"
                  },
                  {
                    "id": "2",
                    "revision": "1",
                    "operation": "INSERT"
                  }
                ]
              }
            }
          },
          "400": {
            "description": "The request was rejected.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "401": {
            "description": "Unauthorized.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "403": {
            "description": "Forbidden.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "404": {
            "description": "NotFound.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "429": {
            "description": "Too many requests.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "495": {
            "description": "Cert Error.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "496": {
            "description": "No Cert.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "503": {
            "description": "Service Unavailable.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "default": {
            "description": "An unexpected error occurred.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          }
        }
      }
    },
    "/app/form/fields.json": {
      "get": {
        "operationId": "getFormFields",
        "summary": "Get the field codes and types of an App",
        "description": "Returns the App's field definitions. Call this before building a `query` so that the correct operators are used for each field type, and so that exact field codes and choice strings are known.",
        "parameters": [
          {
            "name": "app",
            "in": "query",
            "required": true,
            "description": "The App ID. This is the number in the App's URL, for example `31` in `https://example.cybozu.com/k/31/`.",
            "type": "integer"
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "The localized language to retrieve the data in:\n- `default`: retrieves the default names\n- `en`: retrieves the localized English names\n- `zh`: retrieves the localized Chinese names\n- `ja`: retrieves the localized Japanese names\n- `user`: retrieves the localized names, in the same language as the language setting* set on the user used for the authentication.If ignored, the default names will be retrieved.",
            "type": "string",
            "enum": [
              "default",
              "user",
              "ja",
              "en",
              "zh"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "`properties` is keyed by field code. Each entry has at least `type`, `code` and `label`.\nUse `type` to decide which query operators are valid for that field (see the `query` parameter of `GET /records.json`).\nSelection fields additionally carry `options`, which lists the exact choice strings that a query must match.",
            "schema": {
              "$ref": "#/definitions/GetFormFieldsResponse"
            }
          },
          "400": {
            "description": "The request was rejected.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "401": {
            "description": "Unauthorized.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "403": {
            "description": "Forbidden.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "404": {
            "description": "NotFound.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "429": {
            "description": "Too many requests.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "495": {
            "description": "Cert Error.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "496": {
            "description": "No Cert.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "503": {
            "description": "Service Unavailable.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          },
          "default": {
            "description": "An unexpected error occurred.",
            "schema": {
              "$ref": "#/definitions/Error"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "GetRecordsResponse": {
      "type": "object",
      "properties": {
        "records": {
          "type": "array",
          "description": "The records that matched the query.",
          "items": {
            "$ref": "#/definitions/Record"
          }
        },
        "totalCount": {
          "type": "string",
          "x-nullable": true,
          "description": "The total number of records matching the query, as a string. `null` unless `totalCount=true` was requested."
        }
      }
    },
    "Record": {
      "type": "object",
      "description": "One record. **The keys are the App's field codes**, which differ per App, so they are not listed in this schema.\nEach value is an object with `type` (the field type) and `value` (the content).\nRead a field as `records[i].<fieldCode>.value`.\n\nThe shape of `value` depends on `type`:\n\n| type | shape of `value` |\n|---|---|\n| `SINGLE_LINE_TEXT` `MULTI_LINE_TEXT` `RICH_TEXT` `LINK` `NUMBER` `CALC` `DATE` `TIME` `DATETIME` `RECORD_NUMBER` `STATUS` `__ID__` `__REVISION__` | string |\n| `CHECK_BOX` `MULTI_SELECT` `CATEGORY` | array of strings |\n| `DROP_DOWN` `RADIO_BUTTON` | string, or `null` when nothing is selected |\n| `USER_SELECT` `ORGANIZATION_SELECT` `GROUP_SELECT` `STATUS_ASSIGNEE` | array of `{ code, name }` |\n| `CREATOR` `MODIFIER` | `{ code, name }` |\n| `FILE` | array of `{ fileKey, name, contentType, size }` |\n| `SUBTABLE` | array of `{ id, value: { <fieldCode>: { type, value } } }` |\n\nField types that hold no data of their own — `GROUP`, `REFERENCE_TABLE`, `LABEL`, `SPACER`, `HR` — are **not returned** in the response even though they exist on the form.",
      "additionalProperties": {
        "$ref": "#/definitions/FieldValue"
      }
    },
    "FieldValue": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "description": "The field type, for example `SINGLE_LINE_TEXT`, `NUMBER`, `DROP_DOWN`, `DATE`, `USER_SELECT`, `SUBTABLE`."
        },
        "value": {
          "description": "The field's content. A string, an array of strings, an object, or an array of objects depending on `type`. See the description of the parent schema."
        }
      }
    },
    "GetFormFieldsResponse": {
      "type": "object",
      "properties": {
        "properties": {
          "type": "object",
          "description": "Field definitions, keyed by field code.",
          "additionalProperties": {
            "$ref": "#/definitions/FieldProperty"
          }
        },
        "revision": {
          "type": "string",
          "description": "The App's settings revision number."
        }
      }
    },
    "FieldProperty": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "description": "The field type. Determines which query operators may be used against this field."
        },
        "code": {
          "type": "string",
          "description": "The field code, used in queries."
        },
        "label": {
          "type": "string",
          "description": "The display name shown on the form."
        },
        "required": {
          "type": "boolean",
          "description": "Whether the field is mandatory."
        },
        "options": {
          "type": "object",
          "description": "For selection fields only. Keyed by the exact choice string that a query must match.",
          "additionalProperties": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string"
              },
              "index": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "AddRecordsRequestBody": {
      "type": "object",
      "required": [
        "app",
        "records"
      ],
      "properties": {
        "app": {
          "type": "integer",
          "description": "The App ID. This is the number in the App's URL, for example `31` in `https://example.cybozu.com/k/31/`."
        },
        "records": {
          "type": "array",
          "maxItems": 100,
          "description": "The records to register, up to 100 per call. Records are registered in the order given here.\nAn empty object (`{}`) registers a record with every field left at its default value.\nA field code that does not exist on the App is silently ignored rather than rejected.",
          "items": {
            "$ref": "#/definitions/RecordInput"
          }
        }
      }
    },
    "RecordInput": {
      "type": "object",
      "description": "One record to write, keyed by the App's field codes (call `GET /app/form/fields.json` first to obtain these). Each value is an object of the form `{ \"value\": <content> }`.\n\nThe shape of `value` depends on the field's type:\n\n| Field type | shape of `value` |\n|---|---|\n| Single-line text, Multi-line text, Rich text, Link, Number, Date, Time, Date and time, Radio button, Drop-down | string (`\"\"` clears a Radio button/Drop-down) |\n| Check box, Multi-choice | array of strings — must exactly match the App's configured choice strings |\n| User selection, Department selection, Group selection | array of `{ \"code\": <login name or org/group code> }` |\n| Attachment | array of `{ \"fileKey\": <key from the file-upload API> }` — include existing `fileKey`s to keep already-attached files |\n| Table (Subtable) | array of `{ \"id\": <existing row id, optional>, \"value\": { <fieldCode>: { \"value\": ... } } }` |\n\nFields that cannot be set through this API — values copied from a Lookup's source, Status, Category, Calculated, Assignee, and an auto-calculated Single-line text field — are ignored if included.\nSetting Created by, Updated by, Created datetime or Updated datetime requires App Management permission on the credential.",
      "additionalProperties": {
        "$ref": "#/definitions/FieldValueInput"
      }
    },
    "FieldValueInput": {
      "type": "object",
      "properties": {
        "value": {
          "description": "The value to write. A string, an array of strings, or an array of objects depending on the field's type — see the description of the parent schema."
        }
      }
    },
    "AddRecordsResponse": {
      "type": "object",
      "properties": {
        "ids": {
          "type": "array",
          "description": "The record IDs assigned to the newly registered records, in the same order as the `records` given in the request.",
          "items": {
            "type": "string"
          }
        },
        "revisions": {
          "type": "array",
          "description": "The revision number of each newly registered record (normally `\"1\"`), in the same order as `ids`.",
          "items": {
            "type": "string"
          }
        }
      }
    },
    "UpdateRecordsRequestBody": {
      "type": "object",
      "required": [
        "app",
        "records"
      ],
      "properties": {
        "app": {
          "type": "integer",
          "description": "The App ID. This is the number in the App's URL, for example `31` in `https://example.cybozu.com/k/31/`."
        },
        "upsert": {
          "type": "boolean",
          "description": "If `true`, run in UPSERT mode: an entry whose target record (identified by `updateKey`) does not exist is registered as a new record instead of failing. Requires every entry to use `updateKey` rather than `id`, and requires record-add permission on the App in addition to the usual record-edit permission. Defaults to `false`.",
          "default": false
        },
        "records": {
          "type": "array",
          "maxItems": 100,
          "description": "The records to update, up to 100 per call. Each entry identifies its target record with exactly one of `id` or `updateKey`.",
          "items": {
            "$ref": "#/definitions/UpdateRecordItem"
          }
        }
      }
    },
    "UpdateRecordItem": {
      "type": "object",
      "description": "One record to update. Provide exactly one of `id` or `updateKey` to identify the target record — never both.",
      "properties": {
        "id": {
          "type": "integer",
          "description": "The record ID to update. Mutually exclusive with `updateKey`. Not valid in UPSERT mode (use `updateKey` there, since a new record has no ID yet)."
        },
        "updateKey": {
          "$ref": "#/definitions/UpdateKey"
        },
        "record": {
          "$ref": "#/definitions/RecordInput",
          "description": "The field values to write. If omitted, the record's existing values are left unchanged. See `RecordInput` for the shape of each field's `value`, and which fields cannot be updated through this API."
        },
        "revision": {
          "type": "integer",
          "description": "The revision number this update expects the record to currently be at. If it does not match, the update is rejected (and the whole batch cancelled). Omit, or pass `-1`, to skip this check."
        }
      }
    },
    "UpdateKey": {
      "type": "object",
      "required": [
        "field",
        "value"
      ],
      "description": "Identifies a record by a field value instead of by record ID. The field must be a Single-line text or Number field configured with duplicates disallowed. Mutually exclusive with `id`.",
      "properties": {
        "field": {
          "type": "string",
          "description": "The field code of the unique-value field to match on."
        },
        "value": {
          "type": "string",
          "description": "The value to match against that field."
        }
      }
    },
    "UpdateRecordsResponse": {
      "type": "object",
      "properties": {
        "records": {
          "type": "array",
          "description": "One entry per updated (or, in UPSERT mode, newly registered) record, in the same order as the `records` given in the request.",
          "items": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The record ID. In UPSERT mode, this is the newly assigned ID when the record was registered rather than updated."
              },
              "revision": {
                "type": "string",
                "description": "The record's revision number after this update."
              },
              "operation": {
                "type": "string",
                "enum": [
                  "UPDATE",
                  "INSERT"
                ],
                "description": "`UPDATE` if an existing record was modified, `INSERT` if UPSERT mode registered a new record."
              }
            }
          }
        }
      }
    },
    "Error": {
      "type": "object",
      "properties": {
        "message": {
          "type": "string",
          "description": "A human-readable description of the error."
        },
        "id": {
          "type": "string",
          "description": "An identifier for the individual error occurrence."
        },
        "code": {
          "type": "string",
          "description": "The error code, for example `CB_VA01`."
        }
      }
    }
  }
}
tips
補足

他のAPIも含みたい場合は、次の記事を参考にしてください。
Microsoftカスタムコネクタ用kintoneのOpenAPI Spec2.0を作成する

次のMicrosoftの公式ドキュメントも参考にしてください。
OpenAPI 定義からカスタム コネクタを作成する (External link)

STEP3:ソリューションの作成

Copilot StudioでAIエージェントを作成する際には、ソリューション設定が求められます。
ソリューション設定が必要な理由は、開発・テスト・本番の環境間移行(ALM)と組織的なガバナンス管理を安全に行うためです。

  1. Power Appsで ソリューションの作成 (External link) の手順にしたがって、ソリューションを作成する。

  2. [NEW solution]をクリックし、任意のソリューション名「例:kintone-copilot」を入力して作成する。

  3. 作成したソリューションの画面で、[New]をクリックし、[Automation]の中の[Custom connector]をクリックする。

STEP4:カスタムコネクタの作成

カスタムコネクタの作成手順の詳細は、次のページも参照してください。
カスタム コネクタを最初から作成する (External link)

  1. STEP3で、Power Automateに遷移して次のようなカスタムコネクタの作成画面が表示される。

  2. 左上のConnector Nameで、任意のコネクタ名「例:kintone-Connector」を入力する。

  3. Swagger editorを開き、 STEP2 で用意したOASsample.jsonをペーストする。

  4. 次の内容を確認する。

    • [General]タブ:Hostが連携したいkintoneのサブドメインになっていること
    • [Security]タブ:認証方式がAPIキーになっていること
    • [Definition]タブ:OASで定義したAPIアクションが表示されていること
  5. [Create connector]をクリックする。

カスタムコネクタの動作確認

  1. 作成したカスタムコネクタの[Test]タブを開く。

  2. Connectionsの[New connection]をクリックする。

  3. STEP1で控えた、kintoneアプリのAPIトークンを入力する。

  4. 作成したカスタムコネクタの[Test]タブ画面を再度開き、次のように表示されていることを確認する。

  5. 各エンドポイントのテストを実行する。
    レコード登録や更新は、[Raw Body]スイッチをONにして次のようにリクエストボディを記入してテストを実行する。

以下は、kintoneのサンプルアプリ「SFA(営業支援)パック」の顧客管理アプリのリクエストボディ例です。

addRecordsのリクエストボディ例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "app": 1,
  "records": [
    {
      "会社名": { "value": "株式会社サンプル商事" },
      "顧客ランク": { "value": "A" },
      "業種": { "value": "卸売業、小売業、飲食店" },
      "郵便番号": { "value": "150-0001" },
      "都道府県": { "value": "東京都" },
      "住所": { "value": "渋谷区神宮前1-1-1" },
      "建物名": { "value": "サンプルビル3F" },
      "電話番号": { "value": "03-1234-5678" },
      "FAX": { "value": "03-1234-5679" },
      "Webサイト": { "value": "https://www.sample-shoji.example.com" },
      "締め日": { "value": "末日" },
      "支払日": { "value": "翌月末日" },
      "顧客情報メモ欄": { "value": "新規開拓リード。展示会にて名刺交換。" }
    },
    {
      "会社名": { "value": "有限会社テスト工業" },
      "顧客ランク": { "value": "B" },
      "業種": { "value": "製造業" },
      "郵便番号": { "value": "460-0008" },
      "都道府県": { "value": "愛知県" },
      "住所": { "value": "名古屋市中区栄2-2-2" },
      "建物名": { "value": "" },
      "電話番号": { "value": "052-987-6543" },
      "FAX": { "value": "052-987-6544" },
      "Webサイト": { "value": "https://www.test-kogyo.example.com" },
      "締め日": { "value": "20日" },
      "支払日": { "value": "翌月25日" },
      "顧客情報メモ欄": { "value": "既存取引先からの紹介案件。" }
    }
  ]
}
updateRecordsのリクエストボディ例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "app": 1,
  "records": [
    {
      "updateKey": {
        "field": "会社名",
        "value": "株式会社サンプル商事"
      },
      "record": {
        "顧客ランク": { "value": "A" },
        "顧客情報メモ欄": { "value": "新規開拓リード。展示会にて名刺交換。初回商談実施済み。" }
      }
    },
    {
      "updateKey": {
        "field": "会社名",
        "value": "有限会社テスト工業"
      },
      "record": {
        "顧客ランク": { "value": "A" },
        "顧客情報メモ欄": { "value": "既存取引先からの紹介案件。見積提出済み、成約見込み高い。" }
      }
    }
  ]
}

Statusが200で返ってきたら、カスタムコネクタの作成は成功です。

STEP5:エージェントの作成

現在、Copilot Studioには複数の「ハーネス(実行基盤)」があり、エージェント作成時にどちらを使うか選択して作成します。

  • 自律型のAIエージェント「Agent(GitHub Copilot)」
  • 従来型のルールベースのAIエージェント「Agent(Standard)」

5.1 エージェントの選択

主なエージェントの違いは次のとおりです。

Agent 特徴 ハーネス 向いている業務
Agent(GitHub Copilot) 人が目的を伝える。エージェントは、ナレッジ、ツール、Skillsなどを組み合わせ、状況に応じて実行計画を自ら考える。 GitHub Copilotハーネス 決められた道ではなく、目的と現在の状況を確認しながら進め方を考えて進めてほしい業務。横断調査や根拠照合が必要な業務。
Agent(Standard) 人が処理を細かく設計する。エージェントは、ルールベースに動くため、同じ入力に対して同じ処理を実行させやすい。 標準ハーネス あらかじめ定義された会話や処理を、できるだけ同じ流れで実行したい業務。一貫性のある予測可能な動作を期待し、即答が必要な定型業務。

たとえば「kintoneの顧客管理アプリと活動履歴アプリをみて、商談が停滞している顧客を分析し、次のアクションを資料にまとめて」のような指示を考えてみます。
このように、都度必要なデータの集め方や分析の切り口をAI自身に考えさせたい場合は「Agent(GitHub Copilot)」が向いています。
一方、社内FAQアプリに登録済みの質問に対して、毎回同じ手順を必ず案内してほしい場合は「Agent(Standard)」が向いていると考えられます。

information

2026年9月2日、Copilot StudioにGitHub Copilotハーネスが追加され、「Agent(GitHub Copilot)」が使えるようになりました。
ハーネスの詳細は、 Copilot Studio のハーネス (External link) を参照してください。

なお、エージェントを選ぶ際は、機能だけでなく、Copilotクレジットの消費についても確認が必要です。
費用は条件によるため、ライセンス、Copilotクレジットの適用条件については、Microsoft公式情報を確認してください。

5.2 エージェントの新規作成

  1. Copilot Studio (External link) ホームで、[Agents]を開き、[New agent]から「Agent(GitHub Copilot)」または「Agent(Standard)」を選択する。
  2. 任意のエージェント名「例:営業アシスタント」を入力する。
  3. 「Agent(Standard)」の場合、作成時に言語、 STEP3 で作成したソリューションを設定する。「Agent(GitHub Copilot)」の場合、エージェント公開前に「Settings」パネルで言語・ソリューションを設定する。

5.3 エージェントの設定

「Agent(GitHub Copilot)」と「Agent(Standard)」で、設定内容や画面が異なるため、最低限kintoneとの連携に必要な内容だけ説明します。

インストラクションの設定

インストラクションには、エージェントの役割や振る舞いを指示します。
次のような観点で内容を盛り込んでおくと、kintoneを安全かつ意図したとおりに動作しやすくなるので任意で参考にしてください。

観点 内容
役割・対応範囲 エージェントが何を担当し、何を担当しないか
対象アプリ・フィールド 操作してよいアプリID、操作してほしくないフィールド
更新・登録前の確認 実行前にユーザーへ変更内容を提示し、承認を得るかどうか
情報の取り扱い 回答や処理ログに含めてよい情報の範囲、個人情報・機密情報の引用をどこまで許容するか
エラー時の対応 エラーをどう分かりやすく伝えるか、解決できないときにエスカレーションする担当者
Toolsの設定

作成したカスタムコネクタをエージェントにツールとして追加します。
先ほどOpenAPI Specで定義したアクションが指定できるようになっているので、必要なツールを追加します。

  1. [Tools]セクションで[Add a tool]ボタンをクリックする。

  2. 作成したカスタムコネクタのアクション(例:Get records)をクリックする。

  3. Connectionで、[Create new connection]をクリックする。

  4. 任意の名前「例:顧客管理・活動履歴・案件管理」、api_keyにアプリのAPIトークンを入力して、[Add and configure]をクリックする。複数アプリへのコネクションの場合は、**カンマ区切りでAPIトークンを入力(順不同)**する。

  5. 作成されたToolの画面で、Connectionが正しいことを確認する。

  6. その他のアクションも、同様にツールを追加する。次のようになれば、ツールの設定は完了です。

その他の設定

上記以外にも、使用するAIモデルや、kintone以外に参照させたいナレッジ(Knowledge)、定型処理をまとめたSkills、会話の流れを制御するTopicsなどを必要に応じて追加できます。
任意で設定してください。

information

「Agent(Standard)」では、Topicsで会話のフローをあらかじめ設計することで、エージェントが行う処理を制御できます。
Topicsを設定しなくてもエージェントは応答しますが、AIへ必ず守らせたい業務フローがある場合は、Topicsで明示的なフローとして定義しておくと動作が安定します。

詳細は、以下を参考にして設定してください。

5.4 エージェントの動作テスト

「Agent(Standard)」では「Test」、「Agent(GitHub Copilot)」では「Preview」と呼ばれる、チャットウィンドウで次のようなテストシナリオを入力し、kintoneアプリと連携できているか確認します。

シナリオ 入力する内容の例
1: 検索 「案件管理アプリと活動履歴アプリから最近の営業活動の成果をまとめてください。」
2: 登録 「〇〇株式会社の「テスト案件」を新規登録してください。入力が必要な項目は答えるので聞いてください。」
3: 更新 「〇〇株式会社の住所を更新してください。会社のホームページを調べて入力してください。」
4: 権限外の操作 「〇〇株式会社の顧客情報を削除してください」(削除は許可していないため、対応できない旨が返ることを確認)
5: 未連携アプリへの問い合わせ 「経費精算アプリの今月の申請状況を教えてください」(連携していないアプリのため、対応できない旨が返ることを確認)
information

Copilot StudioのEvaluation機能を使うと、エージェントの自動テストができます。
kintoneとの接続に失敗する場合は、ConnectionのStatusに問題があるかもしれません。
エージェントのSettingsからConnection Settingsを開いて、ステータスが「Connected」になっていることを確認してください。

STEP6:作成したエージェントの公開

6.1 エージェントの公開設定

次のページの手順にしたがって、作成したAIエージェントをMicrosoft 365やMicrosoft Teamsで使えるようにしましょう。
TeamsアプリストアまたはMicrosoft 365エージェントストアでエージェントを表示する (External link)

  1. Copilot Studio (External link) で作成したエージェントを開き、[Publish]をクリックして公開する。

  2. [Channels]をクリックし、Microsoft channelsの「Microsoft 365 and Microsoft Teams」をクリックする。

  3. [Add channel]ボタンをクリックする。

  4. [Availability options]ボタンをクリックし、組織内で共有するユーザーを設定する。

6.2 Microsoft 365 Copilotからエージェントを利用する

  1. Microsoft 365 (External link) を開く。
  2. Agent Store (External link) 画面で、追加したカスタムエージェントの名前を検索し、追加する。

作成したエージェントを実際に使ってみましょう。

顧客情報の検索を依頼

顧客管理アプリのレコードを検索するよう依頼すると、次のように回答してくれます。

権限外の操作を依頼

編集権限のないアプリの更新や、連携していないアプリへの操作を依頼すると、次のように対応できない旨を案内してくれます。

kintoneのデータを取得して分析、グラフ化まで依頼

「Agent(Standard)」と「Agent(GitHub Copilot)」のエージェントを比較しました。
この例のように、AI自身に「どのデータをどうわかりやすく出力するか」を考えさせる指示には、「Agent(GitHub Copilot)」が適切に対応してくれました。

上記の指示で実際にエージェントが作成した図はこちらです。

このように、公開したエージェントは、許可した範囲でkintoneのデータを的確に扱い、範囲外の依頼は正しく断ってくれることを確認できました。

おわりに

Copilot Studioで、kintoneと連携した自社専用のカスタムAIエージェントを構築する例を紹介しました。
今回は、SFA(営業支援)のアプリを使用して説明しましたが、たとえば以下のようなユースケースでも利便性を得られます。

  • 受注に関わるアプリのデータを横断的にみて、受注・出荷管理業務を対応するエージェント
  • kintoneの問い合わせ管理アプリに蓄積された過去の対応履歴をみて、似た問い合わせが来た際に対応するエージェント

今回はシンプルな例として、チャットでの回答にとどめましたが、検索したデータを他のデータベースに保存する、関係者へメールを自動返信するといった業務効率化・自動化を行うエージェントも作成できます。
本記事がCopilotとkintoneを連携して、ビジネスの現場でAIを活用するヒントになれば幸いです。

tips
補足

カスタムエージェントが向いているのは、あいまいな判断を伴う業務です。
判断を伴わない定型処理の場合は、本記事で作成したカスタムコネクタをそのままPower Automateのフローで使い、自動化したほうがシンプルな場合もあります。
業務の性質に合わせて、適切な方法を選びましょう。

information

このTipsは、2026年10月版kintoneで動作を確認しています。

公式コミュニティ

kintone開発者同士で質問や知見を共有し、学び合うことができます。