After getting the meta box working I realised that the code hooked into
save_post wasn't being triggered when posting via the Workflow app.
The app is probably using the WordPress REST API to create the post which doesn't behave in the same way as native posting and bypasses the hook.
John Johnston suggested hooking into
wp_insert_post_data instead for posts made in this way.
I didn't know anything about this method so had to do a bit of research but discovered that it was actually pretty simple to use.
The main caveat was that, as I needed to access the post ID to work with the meta data, I had to pass the second, optional parameter
$postarr to my function or it wouldn't work.
add_filter( 'wp_insert_post_data', 'filter_post_data', '99', 2 );
function filter_post_data( $content, $postarr )
It was then largely a case of repurposing the same code from the
save_post hook to build the updated post content and return it from the function:
$content['post_content'] = /* new content value here */;
It seemed to be working when viewing the post preview but when going to edit the post draft in wp-admin only the original post content was showing.
And I couldn't work out why.
I broke the site a few (a lot) times trying to figure it out before, eventually, realising that the content displayed by the front end and the edit post page are actually saved in different places.
The post as seen by the reader takes its content from the
post_content field but the edit post screen pulls its version from
post_content_filtered - I had been updating the former but not the latter, hence the confusion.
With this accounted for everything works as planned either locally within WordPress or when posted from Workflow.