diff --git a/doc/src/sgml/custom-scan.sgml b/doc/src/sgml/custom-scan.sgml
index a229326836a7b46bdb141192484c1da13d9e53cf..5bba1256d3199f01be999da4fff74eda6d56bba2 100644
--- a/doc/src/sgml/custom-scan.sgml
+++ b/doc/src/sgml/custom-scan.sgml
@@ -82,8 +82,10 @@ typedef struct CustomPath
     by <literal>nodeToString</>, so that debugging routines that attempt to
     print the custom path will work as designed.  <structfield>methods</> must
     point to a (usually statically allocated) object implementing the required
-    custom path methods, of which there are currently only two, as further
-    detailed below.
+    custom path methods, of which there is currently only one.  The
+    <structfield>LibraryName</> and <structfield>SymbolName</> fields must also
+    be initialized so that the dynamic loader can resolve them to locate the
+    method table.
   </para>
 
   <para>
@@ -218,18 +220,6 @@ Node *(*CreateCustomScanState) (CustomScan *cscan);
     the <function>BeginCustomScan</> callback will be invoked to give the
     custom scan provider a chance to do whatever else is needed.
    </para>
-
-   <para>
-<programlisting>
-void (*TextOutCustomScan) (StringInfo str,
-                           const CustomScan *node);
-</programlisting>
-    Generate additional output when <function>nodeToString</> is invoked on
-    this custom plan node.  This callback is optional.  Since
-    <function>nodeToString</> will automatically dump all fields in the
-    structure, including the substructure of the <quote>custom</> fields,
-    there is usually not much need for this callback.
-   </para>
   </sect2>
  </sect1>
 
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index c230ee8296c4609344b4311602d0889593a2f020..012c14bf29af37ff48897edbc85a0cb79f1db1a7 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -613,10 +613,11 @@ _outCustomScan(StringInfo str, const CustomScan *node)
 	WRITE_NODE_FIELD(custom_private);
 	WRITE_NODE_FIELD(custom_scan_tlist);
 	WRITE_BITMAPSET_FIELD(custom_relids);
+	/* Dump library and symbol name instead of raw pointer */
 	appendStringInfoString(str, " :methods ");
-	_outToken(str, node->methods->CustomName);
-	if (node->methods->TextOutCustomScan)
-		node->methods->TextOutCustomScan(str, node);
+	_outToken(str, node->methods->LibraryName);
+	appendStringInfoChar(str, ' ');
+	_outToken(str, node->methods->SymbolName);
 }
 
 static void
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 5e258c939f2c484d7f1579478da175a62cd65c9e..222e2ed31080e3e2674e0fd30686d2cf6fc0bc2a 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -28,6 +28,7 @@
 
 #include <math.h>
 
+#include "fmgr.h"
 #include "nodes/parsenodes.h"
 #include "nodes/plannodes.h"
 #include "nodes/readfuncs.h"
@@ -1806,6 +1807,44 @@ _readForeignScan(void)
 	READ_DONE();
 }
 
+/*
+ * _readCustomScan
+ */
+static CustomScan *
+_readCustomScan(void)
+{
+	READ_LOCALS(CustomScan);
+	char	   *library_name;
+	char	   *symbol_name;
+	const CustomScanMethods *methods;
+
+	ReadCommonScan(&local_node->scan);
+
+	READ_UINT_FIELD(flags);
+	READ_NODE_FIELD(custom_plans);
+	READ_NODE_FIELD(custom_exprs);
+	READ_NODE_FIELD(custom_private);
+	READ_NODE_FIELD(custom_scan_tlist);
+	READ_BITMAPSET_FIELD(custom_relids);
+
+	/*
+	 * Reconstruction of methods using library and symbol name
+	 */
+	token = pg_strtok(&length);		/* skip methods: */
+	token = pg_strtok(&length);		/* LibraryName */
+	library_name = nullable_string(token, length);
+	token = pg_strtok(&length);		/* SymbolName */
+	symbol_name = nullable_string(token, length);
+
+	methods = (const CustomScanMethods *)
+		load_external_function(library_name, symbol_name, true, NULL);
+	Assert(strcmp(methods->LibraryName, library_name) == 0 &&
+		   strcmp(methods->SymbolName, symbol_name) == 0);
+	local_node->methods = methods;
+
+	READ_DONE();
+}
+
 /*
  * ReadCommonJoin
  *	Assign the basic stuff of all nodes that inherit from Join
@@ -2362,6 +2401,8 @@ parseNodeString(void)
 		return_value = _readWorkTableScan();
 	else if (MATCH("FOREIGNSCAN", 11))
 		return_value = _readForeignScan();
+	else if (MATCH("CUSTOMSCAN", 10))
+		return_value = _readCustomScan();
 	else if (MATCH("JOIN", 4))
 		return_value = _readJoin();
 	else if (MATCH("NESTLOOP", 8))
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 292219db51f541de86b352077ccdb75185778ed1..37086c65903bc11915e760483ff639d897e62ca7 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -557,12 +557,11 @@ struct CustomScan;
 typedef struct CustomScanMethods
 {
 	const char *CustomName;
+	const char *LibraryName;
+	const char *SymbolName;
 
 	/* Create execution state (CustomScanState) from a CustomScan plan node */
 	Node	   *(*CreateCustomScanState) (struct CustomScan *cscan);
-	/* Optional: print custom_xxx fields in some special way */
-	void		(*TextOutCustomScan) (StringInfo str,
-											  const struct CustomScan *node);
 } CustomScanMethods;
 
 typedef struct CustomScan