diff --git a/src/cam/capture-script.yaml b/src/cam/capture-script.yaml
index 6a749bc60cf7..dbea4a9f01a7 100644
--- a/src/cam/capture-script.yaml
+++ b/src/cam/capture-script.yaml
@@ -5,6 +5,20 @@
 # A capture script allows to associate a list of controls and their values
 # to frame numbers.
 
+# The script allows to define a list of frames associated with controls and
+# an optional list of properties that controls the script behaviour
+#
+# properties:
+#   - loop: idx
+#     Repeat the controls every 'idx' frames.
+#
+#  frames:
+#    - frameid:
+#        Control1: value1
+#        Control2: value2
+#
+#    List of frame ids with associated a list of controls to be applied
+#
 # \todo Formally define the capture script structure with a schema
 
 # Notes:
@@ -12,35 +26,17 @@
 #   libcamera::controls:: enumeration
 # - Controls not supported by the camera currently operated are ignored
 # - Frame numbers shall be monotonically incrementing, gaps are allowed
+# - If a loop limit is specified, frame numbers in the 'frames' list shall be
+#   strictly minor than the loop control
 
-# Example:
-frames:
-  - 1:
-      Brightness: 0.0
+# Example: Turn brightness up and down every 50 frames
 
-  - 40:
-      Brightness: 0.2
+properties:
+  - loop: 50
 
-  - 80:
-      Brightness: 0.4
-
-  - 120:
-      Brightness: 0.8
-
-  - 160:
-      Brightness: 0.4
-
-  - 200:
-      Brightness: 0.2
-
-  - 240:
+frames:
+  - 0:
       Brightness: 0.0
 
-  - 280:
-      Brightness: -0.2
-
-  - 300:
-      Brightness: -0.4
-
-  - 340:
-      Brightness: -0.8
+  - 25:
+      Brightness: 0.8
diff --git a/src/cam/capture_script.cpp b/src/cam/capture_script.cpp
index 5e85b3ca604c..52bf19961c17 100644
--- a/src/cam/capture_script.cpp
+++ b/src/cam/capture_script.cpp
@@ -15,7 +15,7 @@ using namespace libcamera;
 
 CaptureScript::CaptureScript(std::shared_ptr<Camera> camera,
 			     const std::string &fileName)
-	: camera_(camera), valid_(false)
+	: camera_(camera), loop_(0), valid_(false)
 {
 	FILE *fh = fopen(fileName.c_str(), "r");
 	if (!fh) {
@@ -44,8 +44,13 @@ CaptureScript::CaptureScript(std::shared_ptr<Camera> camera,
 const ControlList &CaptureScript::frameControls(unsigned int frame)
 {
 	static ControlList controls{};
+	unsigned int idx = frame;
 
-	auto it = frameControls_.find(frame);
+	/* If we loop, repeat the controls every 'loop_' frames. */
+	if (loop_)
+		idx = frame % loop_;
+
+	auto it = frameControls_.find(idx);
 	if (it == frameControls_.end())
 		return controls;
 
@@ -149,7 +154,11 @@ int CaptureScript::parseScript(FILE *script)
 
 		std::string section = eventScalarValue(event);
 
-		if (section == "frames") {
+		if (section == "properties") {
+			ret = parseProperties();
+			if (ret)
+				return ret;
+		} else if (section == "frames") {
 			ret = parseFrames();
 			if (ret)
 				return ret;
@@ -161,6 +170,64 @@ int CaptureScript::parseScript(FILE *script)
 	}
 }
 
+int CaptureScript::parseProperty()
+{
+	EventPtr event = nextEvent(YAML_MAPPING_START_EVENT);
+	if (!event)
+		return -EINVAL;
+
+	std::string prop = parseScalar();
+	if (prop.empty())
+		return -EINVAL;
+
+	if (prop == "loop") {
+		event = nextEvent();
+		if (!event)
+			return -EINVAL;
+
+		std::string value = eventScalarValue(event);
+		if (value.empty())
+			return -EINVAL;
+
+		loop_ = atoi(value.c_str());
+		if (!loop_) {
+			std::cerr << "Invalid loop limit: " << loop_ << std::endl;
+			return -EINVAL;
+		}
+	} else {
+		std::cerr << "Unsupported property: " << prop << std::endl;
+		return -EINVAL;
+	}
+
+	event = nextEvent(YAML_MAPPING_END_EVENT);
+	if (!event)
+		return -EINVAL;
+
+	return 0;
+}
+
+int CaptureScript::parseProperties()
+{
+	EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);
+	if (!event)
+		return -EINVAL;
+
+	while (1) {
+		if (event->type == YAML_SEQUENCE_END_EVENT)
+			return 0;
+
+		int ret = parseProperty();
+		if (ret)
+			return ret;
+
+		event = nextEvent();
+		if (!event)
+			return -EINVAL;
+	}
+
+	return 0;
+}
+
 int CaptureScript::parseFrames()
 {
 	EventPtr event = nextEvent(YAML_SEQUENCE_START_EVENT);
@@ -191,6 +258,12 @@ int CaptureScript::parseFrame(EventPtr event)
 		return -EINVAL;
 
 	unsigned int frameId = atoi(key.c_str());
+	if (loop_ && frameId >= loop_) {
+		std::cerr
+			<< "Frame id (" << frameId << ") shall be smaller than"
+			<< "loop limit (" << loop_ << ")" << std::endl;
+		return -EINVAL;
+	}
 
 	event = nextEvent(YAML_MAPPING_START_EVENT);
 	if (!event)
diff --git a/src/cam/capture_script.h b/src/cam/capture_script.h
index fffe67e5a3df..7a0ddebb00b5 100644
--- a/src/cam/capture_script.h
+++ b/src/cam/capture_script.h
@@ -40,6 +40,7 @@ private:
 	std::map<unsigned int, libcamera::ControlList> frameControls_;
 	std::shared_ptr<libcamera::Camera> camera_;
 	yaml_parser_t parser_;
+	unsigned int loop_;
 	bool valid_;
 
 	EventPtr nextEvent(yaml_event_type_t expectedType = YAML_NO_EVENT);
@@ -49,6 +50,8 @@ private:
 
 	int parseScript(FILE *script);
 
+	int parseProperties();
+	int parseProperty();
 	int parseFrames();
 	int parseFrame(EventPtr event);
 	int parseControl(EventPtr event, libcamera::ControlList &controls);
