Salesforceスケジュールフローのスケジュール一覧を取得する
こんにちは。
最近、ダンダダンと薬屋のひとりごとを読んでいるmachaです。
背景
最近ローコード化を推進するSalesforceではフローの機能がだいぶ充実してきました。
なかでもスケジュールフローがありがたい。Apexでスケジューラー実装するの面倒でしたもん。毎朝Slack通知したり、毎晩データリフレッシュしてみたり。気軽に作れるようになりました。
なのですが、スケジュールフローをたくさん作ってしまい何曜日になんの処理が走るのか判らなくなってしまいました。今回は「スケジュールフローのスケジュール一覧」を作ってみたいと思います。
調査
スケジュールフローに関連するオブジェクト
スケジュールフローに関わるオブジェクトを探してみました
FlowDefinitionView フロー定義の説明
いわゆるフローの一覧を取得できました。今回はスケジュールフローで絞り込みました
SELECT Id, ApiName, Label, IsActive, VersionNumber
FROM FlowDefinitionView
WHERE TriggerType='Scheduled' AND IsActive=true
1 3ddGC000000IS2TYAW DaySchedule7 【毎日AM7:00】スケジュールフロー true 2
2 3ddGC000000IS2OYAW ScheduleMonday 【毎週月曜日】スケジュールフロー true 1
3 3ddGC000000IS2KYAW ScheduleTuesday1 【毎週火曜日】フロー true 2
4 3ddGC00010238f3YAA Orch Orchestration flow for Recurrence Scheduler true 1
CronTrigger スケジュール済みジョブ一覧
CronExpressionが馴染みのあるCronコマンドになっていました。加えてNextFireTime項目で次回実行時間も取れたので今回ほしかった「何曜日になんの処理が走るのか」の情報は揃いました。
ただしこのオブジェクトはスケジュール済みジョブがすべて出力されてしまうので後述するCronJobDetailオブジェクトとリレーションして絞り込む必要があります。
SELECT
Id,
CronJobDetailId,
NextFireTime,
State,
CronExpression
FROM CronTrigger
1 08eGC00004mJxzwYAC 08aGC00004lWQoBYAW 2025-03-17T14:55:00.000+0000 WAITING 0 55 7 ? * 1,2,3,4,5,6,7
2 08eGC00004pqtNOYAY 08aGC00004pJvnjYAC 2025-03-23T22:00:00.000+0000 WAITING 0 0 7 ? * * *
3 08eGC00004pqvo6YAA 08aGC00004pJyEMYA0 2025-03-23T20:30:00.000+0000 WAITING 0 30 5 ? * 2 *
4 08eGC00004pqvoLYAQ 08aGC00004pJyEbYAK 2025-03-23T20:30:00.000+0000 WAITING 0 30 5 ? * 2 *
CronJobDetail スケジュール済みジョブの詳細
JobTypeでジョブ種別が取得できました。
CronJobDetail.JobType
1 — データのエクスポート
3 — ダッシュボードの更新
4 — レポート作成スナップショット
6 — Scheduler フロー
7 — スケジュール済みの Apex
8 — レポート実行
9 — 一括処理ジョブ
A — レポート通知
今回取得したいのはスケジュールフローなのでJobType=6でよさそうです。
SELECT Id,Name,JobType
FROM CronJobDetail
WHERE JobType = '6'
Id Name JobType
1 08aGC00004pJvnjYAC DaySchedule7-2 6
2 08aGC00004pJyEMYA0 ScheduleTuesday1-2 6
3 08aGC00004pJyEbYAK ScheduleMonday-1 6
(番外編)FlowRecord フローの詳細
FlowDefinitionViewと似たテーブルがありました。
どうやらFlowDefinitionとFlowDefinitionView.Idでリレーションしている様子。
ただ、、、使い道が解らない。
SELECT Id,Name, Description, FlowCategory, FlowDefinition, FlowSubcategory, ProgressStatus, ScheduledStartDate, Type
FROM FlowRecord
WHERE Type = 'Scheduled'
このオブジェクトは、Salesforce Starter エディションで使用できます。
なるほどStarterエディションで使うオブジェクトでしたか。今回は調査対象外。
調査まとめ
オブジェクトを調べていて気づいたこと。なんとフロー定義「FlowDefinitionView」とスケジュール済みジョブ「CronTrigger」がリレーションしていませんでした。あれれ?!なにかを見落としているんでしょうか
どうすりゃいいの?!
どうやらCronJobDetail.Nameが「スケジュールフローAPI名 & "-" & スケジュールフローバージョン番号」でできているようなのでこれをキーとして利用できるか試してみました。
実装
goのsimpleforceを使って3つのオブジェクトを使ってスケジュールフローがいつ実行されるか試してみました。
-- CronJobDetailId = CronJobDetail.Id
SELECT
Id,
CronJobDetailId,
NextFireTime,
State,
CronExpression
FROM CronTrigger
-- ジョブ詳細
-- Name = FlowDefinitionView.ApiName & "-" & FlowDefinitionView.VersionNumber
-- JobType = 6 はスケジュールフローを表す
SELECT Id,Name,JobType
FROM CronJobDetail
WHERE JobType = '6'
-- スケジュールフローの一覧SOQL
-- TriggerType = 'Scheduled' はスケジュールフローを表す
SELECT Id, ApiName, Label, IsActive, VersionNumber
FROM FlowDefinitionView
WHERE TriggerType='Scheduled' AND IsActive=true
package main
import (
"fmt"
"log"
"github.com/simpleforce/simpleforce"
)
/**
* Salesforceの接続情報
*/
var (
// https://xxxxx.my.salesforce.com
sfURL = "URL"
// ユーザー名とメールアドレスは別物なので要注意
sfUser = "ユーザー名"
sfPassword = "パスワード"
// IPアドレス制限を利用した場合はトークン不要
sfToken = ""
)
// CronTrigger の構造体
type CronTrigger struct {
ID string
CronJobDetailID string
CronExpression string
State string
NextFireTime string
}
// CronJobDetail の構造体
type CronJobDetail struct {
ID string
Name string
}
// FlowDefinition の構造体
type FlowDefinition struct {
ID string
ApiName string
Label string
IsActive bool
VersionNumber int
}
// 結合後のデータ構造
type MergedData struct {
CronJobID string
CronJobName string
CronExpression string
State string
NextFireTime string
FlowID string
ApiName string
Label string
IsActive bool
VersionNumber int
}
// Salesforce に接続
func createClient() *simpleforce.Client {
client := simpleforce.NewClient(sfURL, simpleforce.DefaultClientID, simpleforce.DefaultAPIVersion)
if client == nil {
log.Fatal("Failed to create Salesforce client")
}
err := client.LoginPassword(sfUser, sfPassword, sfToken)
if err != nil {
log.Fatal("Failed to login to Salesforce: ", err)
}
return client
}
// CronTrigger のデータ取得
func getCronTriggers(client *simpleforce.Client) ([]CronTrigger, error) {
query := "SELECT Id, CronJobDetailId, NextFireTime, State, CronExpression FROM CronTrigger"
records, err := client.Query(query)
if err != nil {
return nil, err
}
var results []CronTrigger
for _, record := range records.Records {
results = append(results, CronTrigger{
ID: record.StringField("Id"),
CronJobDetailID: record.StringField("CronJobDetailId"),
CronExpression: record.StringField("CronExpression"),
State: record.StringField("State"),
NextFireTime: record.StringField("NextFireTime"),
})
}
return results, nil
}
// CronJobDetail のデータ取得
func getCronJobDetails(client *simpleforce.Client) (map[string]string, error) {
query := "SELECT Id, Name FROM CronJobDetail WHERE JobType = '6'"
records, err := client.Query(query)
if err != nil {
return nil, err
}
details := make(map[string]string)
for _, record := range records.Records {
details[record.StringField("Id")] = record.StringField("Name")
}
return details, nil
}
// FlowDefinition のデータ取得
func getFlowDefinitions(client *simpleforce.Client) (map[string]FlowDefinition, error) {
query := "SELECT Id, ApiName, Label, IsActive, VersionNumber FROM FlowDefinitionView WHERE TriggerType='Scheduled' AND IsActive=true"
records, err := client.Query(query)
if err != nil {
return nil, err
}
flowMap := make(map[string]FlowDefinition)
for _, record := range records.Records {
isActive := record.BoolField("IsActive")
apiName := record.StringField("ApiName")
versionNumber := record.IntField("VersionNumber")
key := fmt.Sprintf("%s-%d", apiName, versionNumber)
flowMap[key] = FlowDefinition{
ID: record.StringField("Id"),
ApiName: apiName,
Label: record.StringField("Label"),
IsActive: isActive,
VersionNumber: versionNumber,
}
}
return flowMap, nil
}
func main() {
client := createClient()
// データ取得
cronTriggers, err := getCronTriggers(client)
if err != nil {
log.Fatal("Failed to retrieve CronTrigger data: ", err)
}
cronJobDetails, err := getCronJobDetails(client)
if err != nil {
log.Fatal("Failed to retrieve CronJobDetail data: ", err)
}
flowDefinitions, err := getFlowDefinitions(client)
if err != nil {
log.Fatal("Failed to retrieve FlowDefinition data: ", err)
}
// データの結合
var mergedResults []MergedData
for _, cron := range cronTriggers {
cronJobName, exists := cronJobDetails[cron.CronJobDetailID]
if !exists {
continue
}
if flow, exists := flowDefinitions[cronJobName]; exists {
mergedResults = append(mergedResults, MergedData{
CronJobID: cron.ID,
CronJobName: cronJobName,
CronExpression: cron.CronExpression,
State: cron.State,
NextFireTime: cron.NextFireTime,
FlowID: flow.ID,
ApiName: flow.ApiName,
Label: flow.Label,
IsActive: flow.IsActive,
VersionNumber: flow.VersionNumber,
})
}
}
// CSV形式で標準出力
fmt.Println("CronJobId,CronJobName,CronExpression,ApiName,Version,NextFireTime")
for _, data := range mergedResults {
fmt.Printf("%s,%s,%s,%s,%d,%s\n",
data.CronJobID, data.CronJobName, data.CronExpression, data.ApiName, data.VersionNumber, data.NextFireTime)
}
}
無事にフロー名称とCronを一緒に表示することができました。
CronJobId,CronJobName,CronExpression,ApiName,Version,NextFireTime
08eGC00004pqtNOYAY,DaySchedule7-2,0 0 7 ? * * *,DaySchedule7,2,2025-03-23T22:00:00.000+0000
08eGC00004pqvo6YAA,ScheduleTuesday1-2,0 30 5 ? * 2 *,ScheduleTuesday1,2,2025-03-23T20:30:00.000+0000
08eGC00004pqvoLYAQ,ScheduleMonday-1,0 30 5 ? * 2 *,ScheduleMonday,1,2025-03-23T20:30:00.000+0000
simpleforceについて
simpleforceはgolangからsalesforceにカンタンにつなぐことができるライブラリです。
以前、調査していますので参考にどうぞ。
今回、simpleforceを使っていていくつか困ったことがありました。
- SOQLサブクエリを読む方法が判らなかった。今回はサブクエリを使わず都度SOQLを実行しました。
- bool型、数値型の数値を読む方法が判らなかった。ローカルのsimpleforceソースに以下追記しました。もう少し動かしてみて問題なさそうだったら本家にプルリクしようと思います。
func (obj *SObject) BoolField(key string) bool {
value := obj.InterfaceField(key)
switch value.(type) {
case bool:
return value.(bool)
default:
return false
}
}
func (obj *SObject) IntField(key string) int {
value := obj.InterfaceField(key)
switch value.(type) {
case int:
return value.(int)
case float64:
return int(value.(float64))
default:
return 0
}
}
まとめ
「スケジュールフローがいつ実行されているか」知るのにそこそこ労力使いました。そんなに需要ないですかね?ま、楽しかったからいいですけど。
文献
大変参考になりました
ありがとうございます
個人的ちょっと便利なSOQL 10選 takaHALさま
Salesforce Developers 標準オブジェクト
ER図を簡単に作成してみよう【DB設計】【Mermaid】 Seiyaさま
Discussion