diff --git a/app/src/SimpleBatch/ExecuteCommand.php b/app/src/SimpleBatch/ExecuteCommand.php
new file mode 100755
index 0000000..3f7bfc6
--- /dev/null
+++ b/app/src/SimpleBatch/ExecuteCommand.php
@@ -0,0 +1,70 @@
+getArgument('batchId'));
+ $options = [
+ 'min' => intval($input->getOption('min')),
+ 'max' => intval($input->getOption('max')),
+ ];
+
+ $workflow = $this->workflowClient->newWorkflowStub(
+ SimpleBatchWorkflowInterface::class,
+ WorkflowOptions::new()
+ ->withWorkflowId(SimpleBatchWorkflowInterface::WORKFLOW_ID . ':' . $batchId)
+ ->withWorkflowExecutionTimeout(CarbonInterval::week())
+ );
+
+ $output->writeln("Starting SimpleBatchWorkflow... ");
+
+ try {
+ $run = $this->workflowClient->start($workflow, $batchId, $options);
+ $output->writeln(
+ sprintf(
+ 'Started: WorkflowID=%s, RunID=%s',
+ $run->getExecution()->getID(),
+ $run->getExecution()->getRunID(),
+ )
+ );
+ } catch (WorkflowExecutionAlreadyStartedException $e) {
+ $output->writeln('Still running');
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/src/SimpleBatch/README.md b/app/src/SimpleBatch/README.md
new file mode 100644
index 0000000..d08a8da
--- /dev/null
+++ b/app/src/SimpleBatch/README.md
@@ -0,0 +1,17 @@
+# Simple batch sample
+
+This sample demonstrates a simple batch processing.
+
+Run the following command to start a batch with a number of items randomly chosen between given min and max values:
+
+```bash
+php ./app/app.php simple-batch:start [--min ] [--max ]
+```
+
+The minimum and maximum item count resp. default to 10 and 20.
+
+Run the following command to show the batch status:
+
+```bash
+php ./app/app.php simple-batch:status
+```
diff --git a/app/src/SimpleBatch/SimpleBatchActivity.php b/app/src/SimpleBatch/SimpleBatchActivity.php
new file mode 100755
index 0000000..5b39701
--- /dev/null
+++ b/app/src/SimpleBatch/SimpleBatchActivity.php
@@ -0,0 +1,109 @@
+logger = new Logger();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBatchItemIds(int $batchId, array $options): array
+ {
+ $minItemCount = $options['min'] ?? 10;
+ $maxItemCount = $options['max'] ?? 20;
+ // Return an array with between $minItemCount and $maxItemCount entries.
+ return array_map(fn(int $itemId) => ($batchId % 100) * 1000 + $itemId,
+ range(101, random_int(100 + $minItemCount, 100 + $maxItemCount)));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function processItem(int $itemId, int $batchId): int
+ {
+ $this->log("Processing item %d of batch %d.", $itemId, $batchId);
+
+ $random = random_int(0, 90);
+ // Wait for max 1 second.
+ usleep($random * 10000);
+
+ // Randomly throw an error.
+ if($random > 30)
+ {
+ throw new Exception(sprintf("Error while processing of item %d of batch %d.", $itemId, $batchId));
+ }
+ return $random;
+ }
+
+ /**
+ * @param int $batchId
+ * @param array $options
+ *
+ * @return void
+ */
+ public function batchProcessingStarted(int $batchId, array $options): void
+ {
+ $this->log("Started processing of batch %d.", $batchId);
+ $this->log("Batch options: %s.", json_encode($options, JSON_PRETTY_PRINT));
+ }
+
+ /**
+ * @param int $batchId
+ * @param array $results
+ *
+ * @return void
+ */
+ public function batchProcessingEnded(int $batchId, array $results): void
+ {
+ $this->log("Ended processing of batch %d.", $batchId);
+ $this->log("Batch results: %s.", json_encode($results, JSON_PRETTY_PRINT));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function itemProcessingStarted(int $itemId, int $batchId): void
+ {
+ $this->log("Started processing of item %d of batch %d.", $itemId, $batchId);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function itemProcessingEnded(int $itemId, int $batchId): void
+ {
+ $this->log("Ended processing of item %d of batch %d.", $itemId, $batchId);
+ }
+
+ /**
+ * @param string $message
+ * @param mixed ...$arg
+ */
+ private function log(string $message, ...$arg)
+ {
+ $this->logger->debug(sprintf($message, ...$arg));
+ }
+}
diff --git a/app/src/SimpleBatch/SimpleBatchActivityInterface.php b/app/src/SimpleBatch/SimpleBatchActivityInterface.php
new file mode 100755
index 0000000..faf43cd
--- /dev/null
+++ b/app/src/SimpleBatch/SimpleBatchActivityInterface.php
@@ -0,0 +1,66 @@
+batchActivity = Workflow::newActivityStub(
+ SimpleBatchActivityInterface::class,
+ ActivityOptions::new()
+ ->withStartToCloseTimeout(CarbonInterval::seconds(10))
+ ->withScheduleToStartTimeout(CarbonInterval::seconds(10))
+ ->withScheduleToCloseTimeout(CarbonInterval::minutes(30))
+ ->withRetryOptions(
+ RetryOptions::new()
+ ->withMaximumAttempts(100)
+ ->withInitialInterval(CarbonInterval::second(10))
+ ->withMaximumInterval(CarbonInterval::seconds(100))
+ )
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function start(int $batchId, array $options)
+ {
+ // Notify the batch processing start.
+ yield $this->batchActivity->batchProcessingStarted($batchId, $options);
+
+ $itemIds = yield $this->batchActivity->getBatchItemIds($batchId, $options);
+
+ $promises = [];
+ foreach($itemIds as $itemId)
+ {
+ // Set the batch item as pending.
+ $this->pending[$itemId] = true;
+ // Process the batch item.
+ $promises[$itemId] = Workflow::async(
+ function() use($itemId, $batchId) {
+ // Notify the item processing start.
+ yield $this->batchActivity->itemProcessingStarted($itemId, $batchId);
+
+ // This activity randomly throws an exception.
+ $output = yield $this->batchActivity->processItem($itemId, $batchId);
+
+ // Notify the item processing end.
+ yield $this->batchActivity->itemProcessingEnded($itemId, $batchId);
+
+ return $output;
+ }
+ )
+ ->then(
+ fn($output) => $this->results[$itemId] = [
+ 'success' => true,
+ 'output' => $output,
+ ],
+ fn(Throwable $e) => $this->results[$itemId] = [
+ 'success' => false,
+ 'message' => $e->getMessage(),
+ ]
+ )
+ // We are calling always() instead of finally() because the Temporal PHP SDK depends on
+ // react/promise 2.9. Need to be changed to finally() after upgrade to react/promise 3.x.
+ ->always(fn() => $this->pending[$itemId] = false);
+ }
+
+ // Wait for all the async calls to terminate.
+ yield Promise::all($promises);
+
+ // Notify the batch processing end.
+ yield $this->batchActivity->batchProcessingEnded($batchId, $this->results);
+
+ return $this->results;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAvailableResults(): array
+ {
+ return $this->results;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getPendingTasks(): array
+ {
+ return array_keys(array_filter($this->pending, fn($pending) => $pending));
+ }
+}
diff --git a/app/src/SimpleBatch/SimpleBatchWorkflowInterface.php b/app/src/SimpleBatch/SimpleBatchWorkflowInterface.php
new file mode 100755
index 0000000..1cba166
--- /dev/null
+++ b/app/src/SimpleBatch/SimpleBatchWorkflowInterface.php
@@ -0,0 +1,31 @@
+getArgument('batchId'));
+
+ /** @var SimpleBatchWorkflowInterface */
+ $workflow = $this->workflowClient->newRunningWorkflowStub(
+ SimpleBatchWorkflowInterface::class,
+ SimpleBatchWorkflowInterface::WORKFLOW_ID . ':' . $batchId
+ );
+
+ $results = $workflow->getAvailableResults();
+ $pending = $workflow->getPendingTasks();
+ $failedCount = count(array_filter($results, fn(array $result) => !$result['success']));
+
+ $output->writeln("SimpleBatchWorkflow (id $batchId) status");
+ $output->writeln(json_encode([
+ 'count' => [
+ 'pending' => count($pending),
+ 'ended' => count($results),
+ 'succeeded' => count($results) - $failedCount,
+ 'failed' => $failedCount,
+ ],
+ 'tasks' => [
+ 'pending' => $pending,
+ 'results' => $results,
+ ],
+ ], JSON_PRETTY_PRINT));
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/src/SimpleBatchChild/ExecuteCommand.php b/app/src/SimpleBatchChild/ExecuteCommand.php
new file mode 100755
index 0000000..b839f11
--- /dev/null
+++ b/app/src/SimpleBatchChild/ExecuteCommand.php
@@ -0,0 +1,70 @@
+getArgument('batchId'));
+ $options = [
+ 'min' => intval($input->getOption('min')),
+ 'max' => intval($input->getOption('max')),
+ ];
+
+ $workflow = $this->workflowClient->newWorkflowStub(
+ SimpleBatchWorkflowInterface::class,
+ WorkflowOptions::new()
+ ->withWorkflowId(SimpleBatchWorkflowInterface::WORKFLOW_ID . ':' . $batchId)
+ ->withWorkflowExecutionTimeout(CarbonInterval::week())
+ );
+
+ $output->writeln("Starting SimpleBatchWorkflow... ");
+
+ try {
+ $run = $this->workflowClient->start($workflow, $batchId, $options);
+ $output->writeln(
+ sprintf(
+ 'Started: WorkflowID=%s, RunID=%s',
+ $run->getExecution()->getID(),
+ $run->getExecution()->getRunID(),
+ )
+ );
+ } catch (WorkflowExecutionAlreadyStartedException $e) {
+ $output->writeln('Still running');
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/src/SimpleBatchChild/README.md b/app/src/SimpleBatchChild/README.md
new file mode 100644
index 0000000..1f3ccf5
--- /dev/null
+++ b/app/src/SimpleBatchChild/README.md
@@ -0,0 +1,19 @@
+# Simple batch with child workflows sample
+
+This sample demonstrates a simple batch processing, with child workflows.
+
+Unlike the `SimpleBatch` sample, using child workflows will prevent from exceeding [the workflow history limits](https://docs.temporal.io/self-hosted-guide/defaults).
+
+Run the following command to start a batch with a number of items randomly chosen between given min and max values:
+
+```bash
+php ./app/app.php simple-batch-child:start [--min ] [--max ]
+```
+
+The minimum and maximum item count resp. default to 10 and 20.
+
+Run the following command to show the batch status:
+
+```bash
+php ./app/app.php simple-batch-child:status
+```
diff --git a/app/src/SimpleBatchChild/SimpleBatchActivity.php b/app/src/SimpleBatchChild/SimpleBatchActivity.php
new file mode 100755
index 0000000..0debd41
--- /dev/null
+++ b/app/src/SimpleBatchChild/SimpleBatchActivity.php
@@ -0,0 +1,109 @@
+logger = new Logger();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getBatchItemIds(int $batchId, array $options): array
+ {
+ $minItemCount = $options['min'] ?? 10;
+ $maxItemCount = $options['max'] ?? 20;
+ // Return an array with between $minItemCount and $maxItemCount entries.
+ return array_map(fn(int $itemId) => ($batchId % 100) * 1000 + $itemId,
+ range(101, random_int(100 + $minItemCount, 100 + $maxItemCount)));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function processItem(int $itemId, int $batchId): int
+ {
+ $this->log("Processing item %d of batch %d.", $itemId, $batchId);
+
+ $random = random_int(0, 90);
+ // Wait for max 1 second.
+ usleep($random % 10000);
+
+ // Randomly throw an error.
+ if($random > 30)
+ {
+ throw new Exception(sprintf("Error while processing of item %d of batch %d.", $itemId, $batchId));
+ }
+ return $random;
+ }
+
+ /**
+ * @param int $batchId
+ * @param array $options
+ *
+ * @return void
+ */
+ public function batchProcessingStarted(int $batchId, array $options): void
+ {
+ $this->log("Started processing of batch %d.", $batchId);
+ $this->log("Batch options: %s.", json_encode($options, JSON_PRETTY_PRINT));
+ }
+
+ /**
+ * @param int $batchId
+ * @param array $results
+ *
+ * @return void
+ */
+ public function batchProcessingEnded(int $batchId, array $results): void
+ {
+ $this->log("Ended processing of batch %d.", $batchId);
+ $this->log("Batch results: %s.", json_encode($results, JSON_PRETTY_PRINT));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function itemProcessingStarted(int $itemId, int $batchId): void
+ {
+ $this->log("Started processing of item %d of batch %d.", $itemId, $batchId);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function itemProcessingEnded(int $itemId, int $batchId): void
+ {
+ $this->log("Ended processing of item %d of batch %d.", $itemId, $batchId);
+ }
+
+ /**
+ * @param string $message
+ * @param mixed ...$arg
+ */
+ private function log(string $message, ...$arg)
+ {
+ $this->logger->debug(sprintf($message, ...$arg));
+ }
+}
diff --git a/app/src/SimpleBatchChild/SimpleBatchActivityInterface.php b/app/src/SimpleBatchChild/SimpleBatchActivityInterface.php
new file mode 100755
index 0000000..aa04c6b
--- /dev/null
+++ b/app/src/SimpleBatchChild/SimpleBatchActivityInterface.php
@@ -0,0 +1,66 @@
+batchActivity = Workflow::newActivityStub(
+ SimpleBatchActivityInterface::class,
+ ActivityOptions::new()
+ ->withStartToCloseTimeout(CarbonInterval::seconds(10))
+ ->withScheduleToStartTimeout(CarbonInterval::seconds(10))
+ ->withScheduleToCloseTimeout(CarbonInterval::minutes(30))
+ ->withRetryOptions(
+ RetryOptions::new()
+ ->withMaximumAttempts(100)
+ ->withInitialInterval(CarbonInterval::second(10))
+ ->withMaximumInterval(CarbonInterval::seconds(100))
+ )
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function processItem(int $itemId, int $batchId)
+ {
+ // Set the item processing as started.
+ yield $this->batchActivity->itemProcessingStarted($itemId, $batchId);
+
+ // This activity randomly throws an exception.
+ $output = yield $this->batchActivity->processItem($itemId, $batchId);
+
+ // Set the item processing as ended.
+ yield $this->batchActivity->itemProcessingEnded($itemId, $batchId);
+
+ return $output;
+ }
+}
diff --git a/app/src/SimpleBatchChild/SimpleBatchChildWorkflowInterface.php b/app/src/SimpleBatchChild/SimpleBatchChildWorkflowInterface.php
new file mode 100644
index 0000000..b87e533
--- /dev/null
+++ b/app/src/SimpleBatchChild/SimpleBatchChildWorkflowInterface.php
@@ -0,0 +1,28 @@
+batchActivity = Workflow::newActivityStub(
+ SimpleBatchActivityInterface::class,
+ ActivityOptions::new()
+ ->withStartToCloseTimeout(CarbonInterval::seconds(10))
+ ->withScheduleToStartTimeout(CarbonInterval::seconds(10))
+ ->withScheduleToCloseTimeout(CarbonInterval::minutes(30))
+ ->withRetryOptions(
+ RetryOptions::new()
+ ->withMaximumAttempts(100)
+ ->withInitialInterval(CarbonInterval::second(10))
+ ->withMaximumInterval(CarbonInterval::seconds(100))
+ )
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function start(int $batchId, array $options)
+ {
+ // Notify the batch processing start.
+ yield $this->batchActivity->batchProcessingStarted($batchId, $options);
+
+ $itemIds = yield $this->batchActivity->getBatchItemIds($batchId, $options);
+
+ $promises = [];
+ foreach($itemIds as $itemId)
+ {
+ // Set the batch item as pending.
+ $this->pending[$itemId] = true;
+
+ $promises[$itemId] = Workflow::newChildWorkflowStub(SimpleBatchChildWorkflowInterface::class)
+ ->processItem($itemId, $batchId)
+ ->then(
+ fn($output) => $this->results[$itemId] = [
+ 'success' => true,
+ 'output' => $output,
+ ],
+ fn(Throwable $e) => $this->results[$itemId] = [
+ 'success' => false,
+ 'message' => $e->getMessage(),
+ ]
+ )
+ // We are calling always() instead of finally() because the Temporal PHP SDK depends on
+ // react/promise 2.9. Will need to change to finally() when upgrading to react/promise 3.x.
+ ->always(fn() => $this->pending[$itemId] = false);
+ }
+
+ // Wait for all the async calls to terminate.
+ yield Promise::all($promises);
+
+ // Notify the batch processing end.
+ yield $this->batchActivity->batchProcessingEnded($batchId, $this->results);
+
+ return $this->results;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getAvailableResults(): array
+ {
+ return $this->results;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getPendingTasks(): array
+ {
+ return array_keys(array_filter($this->pending, fn($pending) => $pending));
+ }
+}
diff --git a/app/src/SimpleBatchChild/SimpleBatchWorkflowInterface.php b/app/src/SimpleBatchChild/SimpleBatchWorkflowInterface.php
new file mode 100755
index 0000000..5c8daed
--- /dev/null
+++ b/app/src/SimpleBatchChild/SimpleBatchWorkflowInterface.php
@@ -0,0 +1,31 @@
+getArgument('batchId'));
+
+ /** @var SimpleBatchWorkflowInterface */
+ $workflow = $this->workflowClient->newRunningWorkflowStub(
+ SimpleBatchWorkflowInterface::class,
+ SimpleBatchWorkflowInterface::WORKFLOW_ID . ':' . $batchId
+ );
+
+ $results = $workflow->getAvailableResults();
+ $pending = $workflow->getPendingTasks();
+ $failedCount = count(array_filter($results, fn(array $result) => !$result['success']));
+
+ $output->writeln("SimpleBatchWorkflow (id $batchId) status");
+ $output->writeln(json_encode([
+ 'count' => [
+ 'pending' => count($pending),
+ 'ended' => count($results),
+ 'succeeded' => count($results) - $failedCount,
+ 'failed' => $failedCount,
+ ],
+ 'tasks' => [
+ 'pending' => $pending,
+ 'results' => $results,
+ ],
+ ], JSON_PRETTY_PRINT));
+
+ return self::SUCCESS;
+ }
+}