diff --git a/include/libcamera/internal/camera.h b/include/libcamera/internal/camera.h
index 0add0428bb5d..a42f03d4c755 100644
--- a/include/libcamera/internal/camera.h
+++ b/include/libcamera/internal/camera.h
@@ -19,6 +19,8 @@
 
 namespace libcamera {
 
+enum class Orientation;
+
 class CameraControlValidator;
 class PipelineHandler;
 class Stream;
@@ -65,6 +67,7 @@ private:
 	std::string id_;
 	std::set<Stream *> streams_;
 	std::set<const Stream *> activeStreams_;
+	Orientation orientation_;
 
 	bool disconnected_;
 	std::atomic<State> state_;
diff --git a/include/libcamera/internal/pipeline_handler.h b/include/libcamera/internal/pipeline_handler.h
index 0d38080369c5..89d10b373cfa 100644
--- a/include/libcamera/internal/pipeline_handler.h
+++ b/include/libcamera/internal/pipeline_handler.h
@@ -9,6 +9,7 @@
 
 #include <memory>
 #include <queue>
+#include <set>
 #include <string>
 #include <sys/types.h>
 #include <vector>
@@ -18,8 +19,12 @@
 #include <libcamera/controls.h>
 #include <libcamera/stream.h>
 
+#include "libcamera/internal/yaml_emitter.h"
+
 namespace libcamera {
 
+enum class Orientation;
+
 class Camera;
 class CameraConfiguration;
 class CameraManager;
@@ -68,6 +73,9 @@ public:
 
 	CameraManager *cameraManager() const { return manager_; }
 
+	void dumpConfiguration(const std::set<const Stream *> &streams,
+			       const Orientation &orientation);
+
 protected:
 	void registerCamera(std::shared_ptr<Camera> camera);
 	void hotplugMediaDevice(MediaDevice *media);
@@ -81,6 +89,11 @@ protected:
 	CameraManager *manager_;
 
 private:
+	enum DumpMode {
+		Controls,
+		Metadata,
+	};
+
 	void unlockMediaDevices();
 
 	void mediaDeviceDisconnected(MediaDevice *media);
@@ -89,6 +102,8 @@ private:
 	void doQueueRequest(Request *request);
 	void doQueueRequests();
 
+	void dumpRequest(Request *request, DumpMode mode);
+
 	std::vector<std::shared_ptr<MediaDevice>> mediaDevices_;
 	std::vector<std::weak_ptr<Camera>> cameras_;
 
@@ -97,6 +112,14 @@ private:
 	const char *name_;
 	unsigned int useCount_;
 
+	YamlRoot controlsEmitter_;
+	YamlDict controlsDict_;
+	YamlList controlsList_;
+
+	YamlRoot metadataEmitter_;
+	YamlDict metadataDict_;
+	YamlList metadataList_;
+
 	friend class PipelineHandlerFactoryBase;
 };
 
diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp
index 7507e9ddae77..9d28b1202397 100644
--- a/src/libcamera/camera.cpp
+++ b/src/libcamera/camera.cpp
@@ -1215,6 +1215,9 @@ int Camera::configure(CameraConfiguration *config)
 		d->activeStreams_.insert(stream);
 	}
 
+	/* TODO Save sensor configuration for dumping it to capture script */
+	d->orientation_ = config->orientation;
+
 	d->setState(Private::CameraConfigured);
 
 	return 0;
@@ -1356,6 +1359,16 @@ int Camera::start(const ControlList *controls)
 
 	ASSERT(d->requestSequence_ == 0);
 
+	/*
+	 * Invoke method in blocking mode to avoid the risk of writing after
+	 * streaming has started.
+	 * This needs to be here as PipelineHandler::start is a virtual function
+	 * so it is impractical to add the dumping there.
+	 * TODO Pass the sensor configuration, once it is supported
+	 */
+	d->pipe_->invokeMethod(&PipelineHandler::dumpConfiguration,
+			       ConnectionTypeBlocking, d->activeStreams_, d->orientation_);
+
 	ret = d->pipe_->invokeMethod(&PipelineHandler::start,
 				     ConnectionTypeBlocking, this, controls);
 	if (ret)
diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp
index e5940469127e..34111589ab22 100644
--- a/src/libcamera/pipeline_handler.cpp
+++ b/src/libcamera/pipeline_handler.cpp
@@ -8,6 +8,7 @@
 #include "libcamera/internal/pipeline_handler.h"
 
 #include <chrono>
+#include <fstream>
 #include <sys/stat.h>
 #include <sys/sysmacros.h>
 
@@ -464,6 +465,8 @@ void PipelineHandler::doQueueRequest(Request *request)
 
 	request->_d()->sequence_ = data->requestSequence_++;
 
+	dumpRequest(request, DumpMode::Controls);
+
 	if (request->_d()->cancelled_) {
 		completeRequest(request);
 		return;
@@ -555,6 +558,8 @@ void PipelineHandler::completeRequest(Request *request)
 
 	request->_d()->complete();
 
+	dumpRequest(request, DumpMode::Metadata);
+
 	Camera::Private *data = camera->_d();
 
 	while (!data->queuedRequests_.empty()) {
@@ -758,6 +763,94 @@ void PipelineHandler::disconnect()
  * \return The CameraManager for this pipeline handler
  */
 
+/**
+ * \brief Dump the camera configuration to YAML format
+ *
+ * Dump to the file path specified in the LIBCAMERA_DUMP_CAPTURE_SCRIPT
+ * environment variable, if any, the Camera configuration in YAML format.
+ */
+void PipelineHandler::dumpConfiguration(const std::set<const Stream *> &streams,
+					const Orientation &orientation)
+{
+	const char *file = utils::secure_getenv("LIBCAMERA_DUMP_CAPTURE_SCRIPT");
+	if (!file)
+		return;
+
+	std::string filePath(file);
+	LOG(Pipeline, Debug) << "Dumping controls in YAML format to: "
+			     << filePath;
+
+	/* Create the YAML roots for controls and metadata output files. */
+
+	controlsEmitter_ = YamlEmitter::root(filePath);
+	controlsDict_ = controlsEmitter_.dict();
+
+	/*
+	 * Metadata needs to go into a separate file because otherwise it'll
+	 * flood the capture script
+	 */
+	filePath += ".metadata";
+	LOG(Pipeline, Debug) << "Dumping metadata in YAML format to: "
+			     << filePath;
+	metadataEmitter_ = YamlEmitter::root(filePath);
+	metadataDict_ = metadataEmitter_.dict();
+	metadataList_ = metadataDict_.list("frames");
+
+	YamlDict configurationDict = controlsDict_.dict("configuration");
+	std::stringstream o;
+	o << orientation;
+	configurationDict["orientation"] = o.str();
+
+	/* \todo Dump Sensor configuration */
+
+	YamlList streamsList = configurationDict.list("streams");
+
+	for (const auto &stream : streams) {
+		const StreamConfiguration &streamConfig = stream->configuration();
+		YamlDict yamlStream = streamsList.dict();
+
+		yamlStream["pixelformat"] = streamConfig.pixelFormat.toString();
+		yamlStream["size"] = streamConfig.size.toString();
+		yamlStream["stride"] = std::to_string(streamConfig.stride);
+		yamlStream["frameSize"] = std::to_string(streamConfig.frameSize);
+		yamlStream["bufferCount"] = std::to_string(streamConfig.bufferCount);
+
+		if (streamConfig.colorSpace)
+			yamlStream["colorSpace"] =
+				streamConfig.colorSpace->toString();
+	}
+}
+
+void PipelineHandler::dumpRequest(Request *request, DumpMode mode)
+{
+	if (!controlsEmitter_.valid())
+		return;
+
+	ControlList &controls = mode == DumpMode::Controls ? request->controls()
+							   : request->metadata();
+	if (controls.empty())
+		return;
+
+	YamlDict yamlFrame;
+	if (mode == DumpMode::Controls) {
+		if (!controlsList_.valid())
+			controlsList_ = controlsDict_.list("frames");
+
+		yamlFrame = controlsList_.dict();
+	} else {
+		yamlFrame = metadataList_.dict();
+	}
+
+	YamlDict yamlCtrls = yamlFrame.dict(std::to_string(request->sequence()));
+
+	const ControlIdMap *idMap = controls.idMap();
+	for (const auto &pair : controls) {
+		const ControlId *ctrlId = idMap->at(pair.first);
+
+		yamlCtrls[ctrlId->name()] = pair.second.toString();
+	}
+}
+
 /**
  * \class PipelineHandlerFactoryBase
  * \brief Base class for pipeline handler factories
