Using Advanced Custom Fields with version control

ACF lets you build metaboxes, Gutenberg blocks, and more with their easy-to-use interface. These custom fields are stored in the database and then rendered in the WordPress backend for you – no code required.

This can become an issue if you’re using a modern development workflow: building locally, testing on a development/staging environment, then deploying to production.  Since you likely aren’t overwriting the production database with the development one, any metaboxes created in development will need to be re-created in production.

ACF has a built-in feature to help called Local JSON. If you create a folder in your theme named acf-json/, ACF will automatically add/update a JSON file for every group of fields. These JSON files can then be version controlled with the rest of your theme.

After pushing the code changes to production, log into the production site, go to Custom Fields in the backend, and click “Sync available” to manually sync the updated metaboxes.

JSON always takes precedence

If a JSON file exists for a field group, it will always be used instead of the database definition, regardless of which has been modified most recently.

The “Sync available” functionality will update the database definition to match the JSON definition, but it’s not necessary to run this. Once the JSON file has been modified, the changes to the metabox are immediate (no sync necessary). As long as the JSON file exists, the metabox will use the settings found in the JSON file.

If you haven’t synced your field groups and the JSON files go away (ex: you change the active theme), the metaboxes will revert back to the older state defined in the database. This is why it’s a good idea to sync them when you are done modifying your metaboxes.

Alternative Approach

I prefer using a core functionality plugin for any site-specific functionality that isn’t directly theme related.  If the website owner would expect something to keep working after changing themes, it belongs in a functionality plugin. 

In my core functionality plugin, I have a acf.php file with the following features:

  1. Store the JSON files in the core functionality plugin, in a acf-json/ folder.
  2. Only display the ACF field editor if WP_LOCAL_DEV === true. I have this constant defined in wp-config.php locally. This prevents clients from accidentally editing or deleting their metaboxes.
  3. Register an options page. I’ve commented it out, but most sites we build leverage an options page so I can quickly turn it on.
  4. Register new blocks. It’s also commented out but easily accessible so I don’t have to keep referring to my article on building Gutenberg blocks with ACF.
 * Advanced Custom Fields
 * @package    CoreFunctionality
 * @version    2.0
 * @author     Bill Erickson <[email protected]>
 * @copyright  Copyright (c) 2018, Bill Erickson
 * @license    GPL-2.0+

class BE_ACF_Customizations {
	public function __construct() {

		// Only allow fields to be edited on development
		if ( ! defined( 'WP_LOCAL_DEV' ) || ! WP_LOCAL_DEV ) {
			add_filter( 'acf/settings/show_admin', '__return_false' );

		// Save fields in functionality plugin
		add_filter( 'acf/settings/save_json', array( $this, 'get_local_json_path' ) );
		add_filter( 'acf/settings/load_json', array( $this, 'add_local_json_path' ) );

		// Register options page
		//add_action( 'init', array( $this, 'register_options_page' ) );

		// Register Blocks
		//add_action('acf/init', array( $this, 'register_blocks' ) );


	 * Define where the local JSON is saved
	 * @return string
	public function get_local_json_path() {
		return EA_DIR . '/acf-json';

	 * Add our path for the local JSON
	 * @param array $paths
	 * @return array
	public function add_local_json_path( $paths ) {
		$paths[] = EA_DIR . '/acf-json';

		return $paths;

	 * Register Options Page
	function register_options_page() {
	    if ( function_exists( 'acf_add_options_page' ) ) {
	        acf_add_options_page( array(
	        	'title'      => __( 'Site Options', 'core-functionality' ),
	        	'capability' => 'manage_options',
	        ) );

	 * Register Blocks
	 * @link
	 * Categories: common, formatting, layout, widgets, embed
	 * Dashicons:
	 * ACF Settings:
	function register_blocks() {

		if( ! function_exists('acf_register_block_type') )

		acf_register_block_type( array(
			'name'				=> 'features',
			'title'				=> __( 'Features', 'core-functionality' ),
			'render_template'		=> 'partials/block-features.php',
			'category'			=> 'formatting',
			'icon'				=> 'awards',
			'mode'				=> 'auto',
			'keywords'			=> array(),
new BE_ACF_Customizations();

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson


  1. Chris Brailsford says

    Quick question Bill: Is there a reason to sync the acf-json files into the database over not? I know ACF’s documentation states “Now that the JSON file exists, ACF will load the relevant field group and field settings from this file which reduces the number of database calls during your page load!”, so they seem to suggest it being faster/more efficient to use the .json files over database calls. Just curious on your thoughts!

  2. Bryce Jacobson says

    Hi Bill,

    This looks awesome. I’m trying to implement this but am a bit confused on the “CORE_FUNCTIONALITY_VERSION” part. I’m not seeing how this is getting set. Is it something higher up in your Core Functionality plugin, or is it a field on an options page? Or is it simply reading the version from line 6 in the gist?

    • Bill Erickson says

      Yes, this is a constant defined in the main plugin file of my core functionality plugin.

      If this is the only functionality you have that’s tied to a version number, you could could add a version number variable directly to this class which might make it easier to follow.

      I’ll update the code snippet to make the external dependencies more clear.

  3. Andrew says

    Really cool stuff Bill!

    Have you found a way to also sync the removal/deletion of fields based off the json? The auto sync is working fine apart from this.

    Thanks for the great ideas and plugin!

    • Bill Erickson says

      I haven’t tried it, but I assumed it didn’t matter whether fields were added or deleted. It looks at the timestamp in the JSON file, and if that’s newer than the one in the database it uses the JSON file version.