Zenn
🎛️

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'

https://developer.salesforce.com/docs/atlas.ja-jp.object_reference.meta/object_reference/sforce_api_objects_flowrecord.htm

このオブジェクトは、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にカンタンにつなぐことができるライブラリです。
以前、調査していますので参考にどうぞ。
https://zenn.dev/vs_blog/articles/d6193c15f18af3

今回、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

ログインするとコメントできます