Update: There is a Salesforce-provided feature that allows scheduling of flows coming with the Winter '20 release.
https://releasenotes.docs.salesforce.com/en-us/winter20/release-notes/rn_forcecom_flow_fbuilder_start.htm
We ultimately implemented the flow in apex for unrelated reasons, but I did find a couple solutions for scheduling flows. In case anyone else is hoping to do something like this, here is what I found.
Flow versioning is always respected if it is started by a non-flow-admin, a user who does not have the "Manage Flow" system permission. They can not run a flow that is not active.
For flow admins, who do have the "Manage Flow" permission, whether the flow respects the active version or uses the most recent version depends on how the flow was started. If a flow admin starts a flow via the Interview.start method in Apex, via the flow URL, or via the "Run" button on the flow's setup page, then the flow will run the most recent version, even if it is not active. Flows can also contain a subflow element that starts another flow. If the main flow was started by an admin in any of these ways, then all subsequent subflows will also run the most recent version.
Autolaunched flows can also be started by Process Builder or the REST API. A scheduled job is Apex, so we can safely schedule a flow if we can invoke a Process Builder or the REST API from Apex.
Solution 1: use Apex to make a callout to the REST API endpoint for starting flows.
global class SchedulableFlow implements Schedulable {
global void execute(SchedulableContext context) {
String flowName = 'myFlow';
String inputs = '{"inputs":[{"inputVar":"value"}]}'; // Set appropriately for your flow
Http http = new Http();
HttpRequest request = new HttpRequest();
request.setEndpoint(Url.getOrgDomainUrl().toExternalForm() + '/services/data/v45.0/actions/custom/flow/' + flowName);
request.setMethod('POST');
request.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
request.setHeader('Content-Type' ,'application/json');
request.setHeader('content-length', String.valueOf(inputs.length()));
request.setBody(inputs);
HttpResponse response = http.send(request);
// do something to handle requests that fail
}
}
Solution 2: use a Process Builder to start the flow, and invoke the process from Apex (via Platform Events).
Process Builders can't directly be invoked by Apex, but they can be invoked by record changes, other Process Builders, or Platform Events. We could tie the process to a record change, and make that change in Apex to start it, but then we are coupling to a record. Thus we use Platform Events.
Create a Platform Event (for these purposes it doesn't need to be integrated with anything else or contain any custom fields). Create a Process Builder which starts when that event occurs. Add an action to the process that will launch your flow. Publish the event from Apex.
global class SchedulableFlow implements Schedulable {
global void execute(SchedulableContext context) {
List<My_Platform_Event__e> events = new List<My_Platform_Event__e>();
events.add(new My_Platform_Event__e());
// Call method to publish events
List<Database.SaveResult> results = EventBus.publish(events);
// Inspect publishing result for each event
for (Database.SaveResult sr : results) {
// do something to handle events that failed to publish
}
}
}