diff --git a/.gitignore b/.gitignore
index f60dda513e4bbf197a03cf88aaa53f4aa21d8e14..63a34beb978d4827fbdd9ac0cee151d3e91afb6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,7 @@
 !/apps/admin_audit
 !/apps/updatenotification
 !/apps/theming
+!/apps/workflowengine
 /apps/files_external/3rdparty/irodsphp/PHPUnitTest
 /apps/files_external/3rdparty/irodsphp/web
 /apps/files_external/3rdparty/irodsphp/prods/test
diff --git a/apps/workflowengine/appinfo/app.php b/apps/workflowengine/appinfo/app.php
new file mode 100644
index 0000000000000000000000000000000000000000..f6f22ce9488715187e3f0fded34f0d6cef2cd523
--- /dev/null
+++ b/apps/workflowengine/appinfo/app.php
@@ -0,0 +1,23 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+$application = new \OCA\WorkflowEngine\AppInfo\Application();
+$application->registerHooksAndListeners();
diff --git a/apps/workflowengine/appinfo/database.xml b/apps/workflowengine/appinfo/database.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b67a41faed20ab470a90b5d69f98d759b033e152
--- /dev/null
+++ b/apps/workflowengine/appinfo/database.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<database>
+	<name>*dbname*</name>
+	<create>true</create>
+	<overwrite>false</overwrite>
+	<charset>utf8</charset>
+
+	<table>
+		<name>*dbprefix*flow_checks</name>
+		<declaration>
+			<field>
+				<name>id</name>
+				<type>integer</type>
+				<default>0</default>
+				<notnull>true</notnull>
+				<autoincrement>1</autoincrement>
+				<length>4</length>
+			</field>
+
+			<field>
+				<name>class</name>
+				<type>text</type>
+				<notnull>true</notnull>
+				<length>256</length>
+			</field>
+			<field>
+				<name>operator</name>
+				<type>text</type>
+				<notnull>true</notnull>
+				<length>16</length>
+			</field>
+			<field>
+				<name>value</name>
+				<type>clob</type>
+				<notnull>false</notnull>
+			</field>
+			<field>
+				<name>hash</name>
+				<type>text</type>
+				<notnull>true</notnull>
+				<length>32</length>
+			</field>
+
+			<index>
+				<name>flow_unique_hash</name>
+				<unique>true</unique>
+				<field>
+					<name>hash</name>
+				</field>
+			</index>
+		</declaration>
+	</table>
+
+	<table>
+		<name>*dbprefix*flow_operations</name>
+		<declaration>
+			<field>
+				<name>id</name>
+				<type>integer</type>
+				<default>0</default>
+				<notnull>true</notnull>
+				<autoincrement>1</autoincrement>
+				<length>4</length>
+			</field>
+
+			<field>
+				<name>class</name>
+				<type>text</type>
+				<notnull>true</notnull>
+				<length>256</length>
+			</field>
+			<field>
+				<name>name</name>
+				<type>text</type>
+				<notnull>true</notnull>
+				<length>256</length>
+			</field>
+			<field>
+				<name>checks</name>
+				<type>clob</type>
+				<notnull>false</notnull>
+			</field>
+			<field>
+				<name>operation</name>
+				<type>clob</type>
+				<notnull>false</notnull>
+			</field>
+		</declaration>
+	</table>
+</database>
diff --git a/apps/workflowengine/appinfo/info.xml b/apps/workflowengine/appinfo/info.xml
new file mode 100644
index 0000000000000000000000000000000000000000..066589c6618a8282cd737990e74a2e7b60f0410f
--- /dev/null
+++ b/apps/workflowengine/appinfo/info.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<info>
+	<id>workflowengine</id>
+	<name>Files Workflow Engine</name>
+	<description></description>
+	<licence>AGPL</licence>
+	<author>Morris Jobke</author>
+	<version>1.0.0</version>
+	<namespace>WorkflowEngine</namespace>
+
+	<category>other</category>
+	<website>https://github.com/nextcloud/server</website>
+	<bugs>https://github.com/nextcloud/server/issues</bugs>
+	<repository type="git">https://github.com/nextcloud/server.git</repository>
+
+	<types>
+		<filesystem/>
+	</types>
+
+	<dependencies>
+		<owncloud min-version="9.2" max-version="9.2" />
+	</dependencies>
+</info>
diff --git a/apps/workflowengine/appinfo/routes.php b/apps/workflowengine/appinfo/routes.php
new file mode 100644
index 0000000000000000000000000000000000000000..69478b1715cb971fd6681cf178dd5f1d958bfa43
--- /dev/null
+++ b/apps/workflowengine/appinfo/routes.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+return [
+	'routes' => [
+		['name' => 'flowOperations#getChecks', 'url' => '/checks', 'verb' => 'GET'], // TODO rm and do via js?
+		['name' => 'flowOperations#getOperations', 'url' => '/operations', 'verb' => 'GET'],
+		['name' => 'flowOperations#addOperation', 'url' => '/operations', 'verb' => 'POST'],
+		['name' => 'flowOperations#updateOperation', 'url' => '/operations/{id}', 'verb' => 'PUT'],
+		['name' => 'flowOperations#deleteOperation', 'url' => '/operations/{id}', 'verb' => 'DELETE'],
+	]
+];
diff --git a/apps/workflowengine/css/admin.css b/apps/workflowengine/css/admin.css
new file mode 100644
index 0000000000000000000000000000000000000000..73ac448cd7b16d5bf4681d1a80ea611800b1bd70
--- /dev/null
+++ b/apps/workflowengine/css/admin.css
@@ -0,0 +1,43 @@
+.workflowengine .operation {
+	padding: 5px;
+	border-bottom: #eee 1px solid;
+	border-left: rgba(0,0,0,0) 1px solid;
+}
+.workflowengine .operation.modified {
+	border-left: rgb(255, 94, 32) 1px solid;
+}
+.workflowengine .operation button {
+	margin-bottom: 0;
+}
+.workflowengine .operation span.info {
+	padding: 7px;
+	color: #eee;
+}
+.workflowengine .rules .operation:nth-last-child(2) {
+	margin-bottom: 5px;
+}
+
+.workflowengine .pull-right {
+	float: right
+}
+
+.workflowengine .operation .msg {
+	border-radius: 3px;
+	margin: 3px 3px 3px 0;
+	padding: 5px;
+	transition: opacity .5s;
+}
+
+.workflowengine .operation .button-delete,
+.workflowengine .operation .button-delete-check {
+	opacity: 0.5;
+	padding: 7px;
+}
+.workflowengine .operation .button-delete:hover,
+.workflowengine .operation .button-delete:focus,
+.workflowengine .operation .button-delete-check:hover,
+.workflowengine .operation .button-delete-check:focus {
+	opacity: 1;
+	cursor: pointer;
+}
+
diff --git a/apps/workflowengine/js/admin.js b/apps/workflowengine/js/admin.js
new file mode 100644
index 0000000000000000000000000000000000000000..f51352c45e4aac93eef99f2b3bd622c2ce7c69be
Binary files /dev/null and b/apps/workflowengine/js/admin.js differ
diff --git a/apps/workflowengine/js/usergroupmembershipplugin.js b/apps/workflowengine/js/usergroupmembershipplugin.js
new file mode 100644
index 0000000000000000000000000000000000000000..2a6068cda90f0ed086e2779947b2d34dca1e85f5
Binary files /dev/null and b/apps/workflowengine/js/usergroupmembershipplugin.js differ
diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php
new file mode 100644
index 0000000000000000000000000000000000000000..c196ecd955c598bf78718d69e9169861a074a0f3
--- /dev/null
+++ b/apps/workflowengine/lib/AppInfo/Application.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\WorkflowEngine\AppInfo;
+
+use OCP\Util;
+use OCP\WorkflowEngine\RegisterCheckEvent;
+
+class Application extends \OCP\AppFramework\App {
+
+	public function __construct() {
+		parent::__construct('workflowengine');
+
+		$this->getContainer()->registerAlias('FlowOperationsController', 'OCA\WorkflowEngine\Controller\FlowOperations');
+	}
+
+	/**
+	 * Register all hooks and listeners
+	 */
+	public function registerHooksAndListeners() {
+		$dispatcher = $this->getContainer()->getServer()->getEventDispatcher();
+		$dispatcher->addListener(
+			'OCP\WorkflowEngine\RegisterCheckEvent',
+			function(RegisterCheckEvent $event) {
+				$event->addCheck(
+					'OCA\WorkflowEngine\Check\UserGroupMembership',
+					'User group membership',
+					['is', '!is']
+				);
+			},
+			-100
+		);
+
+		$dispatcher->addListener(
+			'OCP\WorkflowEngine::loadAdditionalSettingScripts',
+			function() {
+				Util::addStyle('workflowengine', 'admin');
+				Util::addScript('workflowengine', 'admin');
+				Util::addScript('workflowengine', 'usergroupmembershipplugin');
+			},
+			-100
+		);
+	}
+}
diff --git a/apps/workflowengine/lib/Check/UserGroupMembership.php b/apps/workflowengine/lib/Check/UserGroupMembership.php
new file mode 100644
index 0000000000000000000000000000000000000000..f437dbfc2d1d0b34356915f969d153dcc0e7d3b9
--- /dev/null
+++ b/apps/workflowengine/lib/Check/UserGroupMembership.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\WorkflowEngine\Check;
+
+
+use OCP\Files\Storage\IStorage;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\WorkflowEngine\ICheck;
+
+class UserGroupMembership implements ICheck {
+
+	/** @var string */
+	protected $cachedUser;
+
+	/** @var string[] */
+	protected $cachedGroupMemberships;
+
+	/** @var IUserSession */
+	protected $userSession;
+
+	/** @var IGroupManager */
+	protected $groupManager;
+
+	/**
+	 * @param IUserSession $userSession
+	 * @param IGroupManager $groupManager
+	 */
+	public function __construct(IUserSession $userSession, IGroupManager $groupManager) {
+		$this->userSession = $userSession;
+		$this->groupManager = $groupManager;
+	}
+
+	/**
+	 * @param IStorage $storage
+	 * @param string $path
+	 */
+	public function setFileInfo(IStorage $storage, $path) {
+		// A different path doesn't change group memberships, so nothing to do here.
+	}
+
+	/**
+	 * @param string $operator
+	 * @param string $value
+	 * @return bool
+	 */
+	public function executeCheck($operator, $value) {
+		$user = $this->userSession->getUser();
+
+		if ($user instanceof IUser) {
+			$groupIds = $this->getUserGroups($user);
+			return ($operator === 'is') === in_array($value, $groupIds);
+		} else {
+			return $operator !== 'is';
+		}
+	}
+
+
+	/**
+	 * @param string $operator
+	 * @param string $value
+	 * @throws \UnexpectedValueException
+	 */
+	public function validateCheck($operator, $value) {
+		if (!in_array($operator, ['is', '!is'])) {
+			throw new \UnexpectedValueException('Invalid operator', 1);
+		}
+
+		if (!$this->groupManager->groupExists($value)) {
+			throw new \UnexpectedValueException('Group does not exist', 2);
+		}
+	}
+
+	/**
+	 * @param IUser $user
+	 * @return string[]
+	 */
+	protected function getUserGroups(IUser $user) {
+		$uid = $user->getUID();
+
+		if ($this->cachedUser !== $uid) {
+			$this->cachedUser = $uid;
+			$this->cachedGroupMemberships = $this->groupManager->getUserGroupIds($user);
+		}
+
+		return $this->cachedGroupMemberships;
+	}
+}
diff --git a/apps/workflowengine/lib/Controller/FlowOperations.php b/apps/workflowengine/lib/Controller/FlowOperations.php
new file mode 100644
index 0000000000000000000000000000000000000000..e0836c727a27b7ae12803bb0eb13d3948688f4be
--- /dev/null
+++ b/apps/workflowengine/lib/Controller/FlowOperations.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\WorkflowEngine\Controller;
+
+use OCA\WorkflowEngine\Manager;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use OCP\WorkflowEngine\RegisterCheckEvent;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+class FlowOperations extends Controller {
+
+	/** @var Manager */
+	protected $manager;
+
+	/** @var EventDispatcherInterface */
+	protected $dispatcher;
+
+	/**
+	 * @param IRequest $request
+	 * @param Manager $manager
+	 * @param EventDispatcherInterface $dispatcher
+	 */
+	public function __construct(IRequest $request, Manager $manager, EventDispatcherInterface $dispatcher) {
+		parent::__construct('workflowengine', $request);
+		$this->manager = $manager;
+		$this->dispatcher = $dispatcher;
+	}
+
+	/**
+	 * @NoCSRFRequired
+	 *
+	 * @return JSONResponse
+	 */
+	public function getChecks() {
+		$event = new RegisterCheckEvent();
+		$this->dispatcher->dispatch('OCP\WorkflowEngine\RegisterCheckEvent', $event);
+
+		return new JSONResponse($event->getChecks());
+	}
+
+	/**
+	 * @NoCSRFRequired
+	 *
+	 * @param string $class
+	 * @return JSONResponse
+	 */
+	public function getOperations($class) {
+		$operations = $this->manager->getOperations($class);
+
+		foreach ($operations as &$operation) {
+			$operation = $this->prepareOperation($operation);
+		}
+
+		return new JSONResponse($operations);
+	}
+
+	/**
+	 * @param string $class
+	 * @param string $name
+	 * @param array[] $checks
+	 * @param string $operation
+	 * @return JSONResponse The added element
+	 */
+	public function addOperation($class, $name, $checks, $operation) {
+		try {
+			$operation = $this->manager->addOperation($class, $name, $checks, $operation);
+			$operation = $this->prepareOperation($operation);
+			return new JSONResponse($operation);
+		} catch (\UnexpectedValueException $e) {
+			return new JSONResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
+		}
+	}
+
+	/**
+	 * @param int $id
+	 * @param string $name
+	 * @param array[] $checks
+	 * @param string $operation
+	 * @return JSONResponse The updated element
+	 */
+	public function updateOperation($id, $name, $checks, $operation) {
+		try {
+			$operation = $this->manager->updateOperation($id, $name, $checks, $operation);
+			$operation = $this->prepareOperation($operation);
+			return new JSONResponse($operation);
+		} catch (\UnexpectedValueException $e) {
+			return new JSONResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
+		}
+	}
+
+	/**
+	 * @param int $id
+	 * @return JSONResponse
+	 */
+	public function deleteOperation($id) {
+		$deleted = $this->manager->deleteOperation((int) $id);
+		return new JSONResponse($deleted);
+	}
+
+	/**
+	 * @param array $operation
+	 * @return array
+	 */
+	protected function prepareOperation(array $operation) {
+		$checkIds = json_decode($operation['checks']);
+		$checks = $this->manager->getChecks($checkIds);
+
+		$operation['checks'] = [];
+		foreach ($checks as $check) {
+			// Remove internal values
+			unset($check['id']);
+			unset($check['hash']);
+
+			$operation['checks'][] = $check;
+		}
+
+		return $operation;
+	}
+}
diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php
new file mode 100644
index 0000000000000000000000000000000000000000..98c34e894ccd0cde28ecdddd5608d924fc697b25
--- /dev/null
+++ b/apps/workflowengine/lib/Manager.php
@@ -0,0 +1,306 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCA\WorkflowEngine;
+
+
+use OCP\AppFramework\QueryException;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Files\Storage\IStorage;
+use OCP\IDBConnection;
+use OCP\IServerContainer;
+use OCP\WorkflowEngine\ICheck;
+use OCP\WorkflowEngine\IManager;
+
+class Manager implements IManager {
+
+	/** @var IStorage */
+	protected $storage;
+
+	/** @var string */
+	protected $path;
+
+	/** @var array[] */
+	protected $operations = [];
+
+	/** @var array[] */
+	protected $checks = [];
+
+	/** @var IDBConnection */
+	protected $connection;
+
+	/** @var IServerContainer|\OC\Server */
+	protected $container;
+
+	/**
+	 * @param IDBConnection $connection
+	 * @param IServerContainer $container
+	 */
+	public function __construct(IDBConnection $connection, IServerContainer $container) {
+		$this->connection = $connection;
+		$this->container = $container;
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function setFileInfo(IStorage $storage, $path) {
+		$this->storage = $storage;
+		$this->path = $path;
+	}
+
+	/**
+	 * @inheritdoc
+	 */
+	public function getMatchingOperations($class, $returnFirstMatchingOperationOnly = true) {
+		$operations = $this->getOperations($class);
+
+		$matches = [];
+		foreach ($operations as $operation) {
+			$checkIds = json_decode($operation['checks'], true);
+			$checks = $this->getChecks($checkIds);
+
+			foreach ($checks as $check) {
+				if (!$this->check($check)) {
+					// Check did not match, continue with the next operation
+					continue 2;
+				}
+			}
+
+			if ($returnFirstMatchingOperationOnly) {
+				return $operation;
+			}
+			$matches[] = $operation;
+		}
+
+		return $matches;
+	}
+
+	/**
+	 * @param array $check
+	 * @return bool
+	 */
+	protected function check(array $check) {
+		try {
+			$checkInstance = $this->container->query($check['class']);
+		} catch (QueryException $e) {
+			// Check does not exist, assume it matches.
+			return true;
+		}
+
+		if ($checkInstance instanceof ICheck) {
+			$checkInstance->setFileInfo($this->storage, $this->path);
+			return $checkInstance->executeCheck($check['operator'], $check['value']);
+		} else {
+			// Check is invalid, assume it matches.
+			return true;
+		}
+	}
+
+	/**
+	 * @param string $class
+	 * @return array[]
+	 */
+	public function getOperations($class) {
+		if (isset($this->operations[$class])) {
+			return $this->operations[$class];
+		}
+
+		$query = $this->connection->getQueryBuilder();
+
+		$query->select('*')
+			->from('flow_operations')
+			->where($query->expr()->eq('class', $query->createNamedParameter($class)));
+		$result = $query->execute();
+
+		$this->operations[$class] = [];
+		while ($row = $result->fetch()) {
+			$this->operations[$class][] = $row;
+		}
+		$result->closeCursor();
+
+		return $this->operations[$class];
+	}
+
+	/**
+	 * @param int $id
+	 * @return array
+	 * @throws \UnexpectedValueException
+	 */
+	protected function getOperation($id) {
+		$query = $this->connection->getQueryBuilder();
+		$query->select('*')
+			->from('flow_operations')
+			->where($query->expr()->eq('id', $query->createNamedParameter($id)));
+		$result = $query->execute();
+		$row = $result->fetch();
+		$result->closeCursor();
+
+		if ($row) {
+			return $row;
+		}
+
+		throw new \UnexpectedValueException('Operation does not exist');
+	}
+
+	/**
+	 * @param string $class
+	 * @param string $name
+	 * @param array[] $checks
+	 * @param string $operation
+	 * @return array The added operation
+	 * @throws \UnexpectedValueException
+	 */
+	public function addOperation($class, $name, array $checks, $operation) {
+		$checkIds = [];
+		foreach ($checks as $check) {
+			$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
+		}
+
+		$query = $this->connection->getQueryBuilder();
+		$query->insert('flow_operations')
+			->values([
+				'class' => $query->createNamedParameter($class),
+				'name' => $query->createNamedParameter($name),
+				'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))),
+				'operation' => $query->createNamedParameter($operation),
+			]);
+		$query->execute();
+
+		$id = $query->getLastInsertId();
+		return $this->getOperation($id);
+	}
+
+	/**
+	 * @param int $id
+	 * @param string $name
+	 * @param array[] $checks
+	 * @param string $operation
+	 * @return array The updated operation
+	 * @throws \UnexpectedValueException
+	 */
+	public function updateOperation($id, $name, array $checks, $operation) {
+		$checkIds = [];
+		foreach ($checks as $check) {
+			$checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']);
+		}
+
+		$query = $this->connection->getQueryBuilder();
+		$query->update('flow_operations')
+			->set('name', $query->createNamedParameter($name))
+			->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds))))
+			->set('operation', $query->createNamedParameter($operation))
+			->where($query->expr()->eq('id', $query->createNamedParameter($id)));
+		$query->execute();
+
+		return $this->getOperation($id);
+	}
+
+	/**
+	 * @param int $id
+	 * @return bool
+	 * @throws \UnexpectedValueException
+	 */
+	public function deleteOperation($id) {
+		$query = $this->connection->getQueryBuilder();
+		$query->delete('flow_operations')
+			->where($query->expr()->eq('id', $query->createNamedParameter($id)));
+		return (bool) $query->execute();
+	}
+
+	/**
+	 * @param int[] $checkIds
+	 * @return array[]
+	 */
+	public function getChecks(array $checkIds) {
+		$checkIds = array_map('intval', $checkIds);
+
+		$checks = [];
+		foreach ($checkIds as $i => $checkId) {
+			if (isset($this->checks[$checkId])) {
+				$checks[$checkId] = $this->checks[$checkId];
+				unset($checkIds[$i]);
+			}
+		}
+
+		if (empty($checkIds)) {
+			return $checks;
+		}
+
+		$query = $this->connection->getQueryBuilder();
+		$query->select('*')
+			->from('flow_checks')
+			->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY)));
+		$result = $query->execute();
+
+		$checks = [];
+		while ($row = $result->fetch()) {
+			$this->checks[(int) $row['id']] = $row;
+			$checks[(int) $row['id']] = $row;
+		}
+		$result->closeCursor();
+
+		// TODO What if a check is missing? Should we throw?
+		// As long as we only allow AND-concatenation of checks, a missing check
+		// is like a matching check, so it evaluates to true and therefor blocks
+		// access. So better save than sorry.
+
+		return $checks;
+	}
+
+	/**
+	 * @param string $class
+	 * @param string $operator
+	 * @param string $value
+	 * @return int Check unique ID
+	 * @throws \UnexpectedValueException
+	 */
+	protected function addCheck($class, $operator, $value) {
+		/** @var ICheck $check */
+		$check = $this->container->query($class);
+		$check->validateCheck($operator, $value);
+
+		$hash = md5($class . '::' . $operator . '::' . $value);
+
+		$query = $this->connection->getQueryBuilder();
+		$query->select('id')
+			->from('flow_checks')
+			->where($query->expr()->eq('hash', $query->createNamedParameter($hash)));
+		$result = $query->execute();
+
+		if ($row = $result->fetch()) {
+			$result->closeCursor();
+			return (int) $row['id'];
+		}
+
+		$query = $this->connection->getQueryBuilder();
+		$query->insert('flow_checks')
+			->values([
+				'class' => $query->createNamedParameter($class),
+				'operator' => $query->createNamedParameter($operator),
+				'value' => $query->createNamedParameter($value),
+				'hash' => $query->createNamedParameter($hash),
+			]);
+		$query->execute();
+
+		return $query->getLastInsertId();
+	}
+}
diff --git a/apps/workflowengine/templates/admin.php b/apps/workflowengine/templates/admin.php
new file mode 100644
index 0000000000000000000000000000000000000000..86ecfe085569ec2ee9732bb1452c3f44c42831b5
--- /dev/null
+++ b/apps/workflowengine/templates/admin.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/** @var array $_ */
+/** @var OC_L10N $l */
+?>
+<div id="workflowengine" class="section workflowengine">
+	<h2 class="inlineblock"><?php p($_['heading']); ?></h2>
+	<script type="text/template" id="operations-template">
+		<div class="operations"></div>
+		<button class="button-add-operation">Add operation</button>
+	</script>
+
+	<script type="text/template" id="operation-template">
+		<div class="operation{{#if hasChanged}} modified{{/if}}">
+			<input type="text" class="operation-name" value="{{operation.name}}">
+			{{! delete only makes sense if the operation is already saved }}
+			{{#if operation.id}}
+			<span class="button-delete pull-right icon-delete"></span>
+			{{/if}}
+			<span class="pull-right info">{{operation.class}} - ID: {{operation.id}} - operation: {{operation.operation}}</span>
+
+			<div class="checks">
+				{{#each operation.checks}}
+				<div class="check" data-id="{{@index}}">
+					<select class="check-class">
+						{{#each ../classes}}
+						<option value="{{class}}" {{selectItem class ../class}}>{{name}}</option>
+						{{/each}}
+					</select>
+					<select class="check-operator">
+						{{#each (getOperators class)}}
+						<option value="{{this}}" {{selectItem this ../operator}}>{{this}}</option>
+						{{/each}}
+					</select>
+					<input type="text" class="check-value" value="{{value}}">
+					<span class="button-delete-check pull-right icon-delete"></span>
+				</div>
+				{{/each}}
+			</div>
+			<button class="button-add">Add check</button>
+			{{#if hasChanged}}
+				{{! reset only makes sense if the operation is already saved }}
+				{{#if operation.id}}
+					<button class="button-reset pull-right">Reset</button>
+				{{/if}}
+				<button class="button-save pull-right">Save</button>
+			{{/if}}
+			{{#if saving}}
+				<span class="icon-loading-small pull-right"></span>
+				<span class="pull-right">Saving ...</span>
+			{{else}}{{#if message}}
+				<span class="msg pull-right {{#if errorMessage}}error{{else}}success{{/if}}">
+					{{message}}{{#if errorMessage}} {{errorMessage}}{{/if}}
+				</span>
+			{{/if}}{{/if}}
+		</div>
+	</script>
+
+	<div class="rules"><span class="icon-loading-small"></span> Loading ...</div>
+</div>
diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature
index 487e025092bc81376fab22bfd897c432f7000bff..d397a2852ad06b98765d0f88c534a50121aa6ae5 100644
--- a/build/integration/features/provisioning-v1.feature
+++ b/build/integration/features/provisioning-v1.feature
@@ -295,6 +295,7 @@ Feature: provisioning
 			| systemtags |
 			| theming |
 			| updatenotification |
+			| workflowengine |
 
 	Scenario: get app info
 		Given As an "admin"
diff --git a/core/shipped.json b/core/shipped.json
index da48d14dc0b2fff07a8e91b8f355d2ea061c65b6..8d3056eb9083389b5c8478097abf2052609d7a34 100644
--- a/core/shipped.json
+++ b/core/shipped.json
@@ -27,11 +27,13 @@
     "updatenotification",
     "user_external",
     "user_ldap",
-    "user_saml"
+    "user_saml",
+    "workflowengine"
   ],
   "alwaysEnabled": [
     "files",
     "dav",
-    "federatedfilesharing"
+    "federatedfilesharing",
+    "workflowengine"
   ]
 }
diff --git a/lib/public/WorkflowEngine/ICheck.php b/lib/public/WorkflowEngine/ICheck.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e3d86caad9f30e806e70cdddb8c898556cd6879
--- /dev/null
+++ b/lib/public/WorkflowEngine/ICheck.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\WorkflowEngine;
+
+
+use OCP\Files\Storage\IStorage;
+
+/**
+ * Interface ICheck
+ *
+ * @package OCP\WorkflowEngine
+ * @since 9.1
+ */
+interface ICheck {
+	/**
+	 * @param IStorage $storage
+	 * @param string $path
+	 * @since 9.1
+	 */
+	public function setFileInfo(IStorage $storage, $path);
+
+	/**
+	 * @param string $operator
+	 * @param string $value
+	 * @return bool
+	 * @since 9.1
+	 */
+	public function executeCheck($operator, $value);
+
+	/**
+	 * @param string $operator
+	 * @param string $value
+	 * @throws \UnexpectedValueException
+	 * @since 9.1
+	 */
+	public function validateCheck($operator, $value);
+}
diff --git a/lib/public/WorkflowEngine/IManager.php b/lib/public/WorkflowEngine/IManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..e53a06ec9299fb929c4091aa93ac49336877fe37
--- /dev/null
+++ b/lib/public/WorkflowEngine/IManager.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\WorkflowEngine;
+
+
+use OCP\Files\Storage\IStorage;
+
+/**
+ * Interface IManager
+ *
+ * @package OCP\WorkflowEngine
+ * @since 9.1
+ */
+interface IManager {
+	/**
+	 * @param IStorage $storage
+	 * @param string $path
+	 * @since 9.1
+	 */
+	public function setFileInfo(IStorage $storage, $path);
+
+	/**
+	 * @param string $class
+	 * @param bool $returnFirstMatchingOperationOnly
+	 * @return array
+	 * @since 9.1
+	 */
+	public function getMatchingOperations($class, $returnFirstMatchingOperationOnly = true);
+}
diff --git a/lib/public/WorkflowEngine/RegisterCheckEvent.php b/lib/public/WorkflowEngine/RegisterCheckEvent.php
new file mode 100644
index 0000000000000000000000000000000000000000..e08aae5fbc0a0f6c05a0f1743324d5bbcd67994b
--- /dev/null
+++ b/lib/public/WorkflowEngine/RegisterCheckEvent.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\WorkflowEngine;
+
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Class RegisterCheckEvent
+ *
+ * @package OCP\WorkflowEngine
+ * @since 9.1
+ */
+class RegisterCheckEvent extends Event {
+
+	/** @var array[] */
+	protected $checks = [];
+
+	/**
+	 * @param string $class
+	 * @param string $name
+	 * @param string[] $operators
+	 * @throws \OutOfBoundsException when the check class is already registered
+	 * @throws \OutOfBoundsException when the provided information is invalid
+	 * @since 9.1
+	 */
+	public function addCheck($class, $name, array $operators) {
+		if (!is_string($class)) {
+			throw new \OutOfBoundsException('Given class name is not a string');
+		}
+
+		if (isset($this->checks[$class])) {
+			throw new \OutOfBoundsException('Duplicate check class "' . $class . '"');
+		}
+
+		if (!is_string($name)) {
+			throw new \OutOfBoundsException('Given check name is not a string');
+		}
+
+		foreach ($operators as $operator) {
+			if (!is_string($operator)) {
+				throw new \OutOfBoundsException('At least one of the operators is not a string');
+			}
+		}
+
+		$this->checks[$class] = [
+			'class' => $class,
+			'name' => $name,
+			'operators' => $operators,
+		];
+	}
+
+	/**
+	 * @return array[]
+	 * @since 9.1
+	 */
+	public function getChecks() {
+		return array_values($this->checks);
+	}
+}
diff --git a/tests/lib/App/ManagerTest.php b/tests/lib/App/ManagerTest.php
index 2d4ec4968b00ac12f647a06c6a0a16621dfd59db..80754413fc81e74b542b68eea02e1a1ac45c481f 100644
--- a/tests/lib/App/ManagerTest.php
+++ b/tests/lib/App/ManagerTest.php
@@ -306,7 +306,7 @@ class ManagerTest extends TestCase {
 		$this->appConfig->setValue('test1', 'enabled', 'yes');
 		$this->appConfig->setValue('test2', 'enabled', 'no');
 		$this->appConfig->setValue('test3', 'enabled', '["foo"]');
-		$this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3'], $this->manager->getInstalledApps());
+		$this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'workflowengine'], $this->manager->getInstalledApps());
 	}
 
 	public function testGetAppsForUser() {
@@ -320,7 +320,7 @@ class ManagerTest extends TestCase {
 		$this->appConfig->setValue('test2', 'enabled', 'no');
 		$this->appConfig->setValue('test3', 'enabled', '["foo"]');
 		$this->appConfig->setValue('test4', 'enabled', '["asd"]');
-		$this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3'], $this->manager->getEnabledAppsForUser($user));
+		$this->assertEquals(['dav', 'federatedfilesharing', 'files', 'test1', 'test3', 'workflowengine'], $this->manager->getEnabledAppsForUser($user));
 	}
 
 	public function testGetAppsNeedingUpgrade() {
@@ -338,6 +338,7 @@ class ManagerTest extends TestCase {
 			'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'],
 			'test4' => ['id' => 'test4', 'version' => '3.0.0', 'requiremin' => '8.1.0'],
 			'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'],
+			'workflowengine' => ['id' => 'workflowengine'],
 		];
 
 		$this->manager->expects($this->any())
@@ -378,6 +379,7 @@ class ManagerTest extends TestCase {
 			'test2' => ['id' => 'test2', 'version' => '1.0.0', 'requiremin' => '8.2.0'],
 			'test3' => ['id' => 'test3', 'version' => '1.2.4', 'requiremin' => '9.0.0'],
 			'testnoversion' => ['id' => 'testnoversion', 'requiremin' => '8.2.0'],
+			'workflowengine' => ['id' => 'workflowengine'],
 		];
 
 		$this->manager->expects($this->any())
diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php
index 2e5b6f74ab7a542e33f53da624ac1e8d4222b9ef..d37b4a0f56a0a77bb311ae4616a763b0aad3a4eb 100644
--- a/tests/lib/AppTest.php
+++ b/tests/lib/AppTest.php
@@ -316,6 +316,7 @@ class AppTest extends \Test\TestCase {
 					'appforgroup12',
 					'dav',
 					'federatedfilesharing',
+					'workflowengine',
 				),
 				false
 			),
@@ -330,6 +331,7 @@ class AppTest extends \Test\TestCase {
 					'appforgroup2',
 					'dav',
 					'federatedfilesharing',
+					'workflowengine',
 				),
 				false
 			),
@@ -345,6 +347,7 @@ class AppTest extends \Test\TestCase {
 					'appforgroup2',
 					'dav',
 					'federatedfilesharing',
+					'workflowengine',
 				),
 				false
 			),
@@ -360,6 +363,7 @@ class AppTest extends \Test\TestCase {
 					'appforgroup2',
 					'dav',
 					'federatedfilesharing',
+					'workflowengine',
 				),
 				false,
 			),
@@ -375,6 +379,7 @@ class AppTest extends \Test\TestCase {
 					'appforgroup2',
 					'dav',
 					'federatedfilesharing',
+					'workflowengine',
 				),
 				true,
 			),
@@ -452,11 +457,11 @@ class AppTest extends \Test\TestCase {
 			);
 
 		$apps = \OC_App::getEnabledApps();
-		$this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing',), $apps);
+		$this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'workflowengine'), $apps);
 
 		// mock should not be called again here
 		$apps = \OC_App::getEnabledApps();
-		$this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing',), $apps);
+		$this->assertEquals(array('files', 'app3', 'dav', 'federatedfilesharing', 'workflowengine'), $apps);
 
 		$this->restoreAppConfig();
 		\OC_User::setUserId(null);