allow workflow to have no steps if it has output
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExecutionContext.java b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExecutionContext.java
index 0abb57c..deef3d0 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExecutionContext.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowExecutionContext.java
@@ -917,7 +917,7 @@
     @JsonIgnore
     List<WorkflowStepDefinition> getStepsResolved() {
         if (stepsResolved ==null) {
-            stepsResolved = MutableList.copyOf(WorkflowStepResolution.resolveSteps(getManagementContext(), WorkflowExecutionContext.this.stepsDefinition));
+            stepsResolved = MutableList.copyOf(WorkflowStepResolution.resolveSteps(getManagementContext(), WorkflowExecutionContext.this.stepsDefinition, outputDefinition));
         }
         return stepsResolved;
     }
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepResolution.java b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepResolution.java
index 515153a..102843d 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepResolution.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/WorkflowStepResolution.java
@@ -45,8 +45,15 @@
 public class WorkflowStepResolution {
 
     public static List<WorkflowStepDefinition> resolveSteps(ManagementContext mgmt, List<Object> steps) {
+        return resolveSteps(mgmt, steps, null);
+    }
+    public static List<WorkflowStepDefinition> resolveSteps(ManagementContext mgmt, List<Object> steps, Object outputDefinition) {
         List<WorkflowStepDefinition> result = MutableList.of();
-        if (steps==null || steps.isEmpty()) throw new IllegalStateException("No steps defined in workflow");
+        if (steps==null || steps.isEmpty()) {
+            if (outputDefinition==null) throw new IllegalStateException("No steps defined in workflow and no output set");
+            // if there is output, an empty workflow makes sense
+            return result;
+        }
         for (int i=0; i<steps.size(); i++) {
             try {
                 result.add(resolveStep(mgmt, steps.get(i)));
@@ -172,7 +179,7 @@
         if (!hasCondition && entityOrAdjunctWhereRunningIfKnown!=null) {
             // ideally try to resolve the steps at entity init time; except if a condition is required we skip that so you can have steps that only resolve late,
             // and if entity isn't available then we don't need that either
-            WorkflowStepResolution.resolveSteps( ((BrooklynObjectInternal)entityOrAdjunctWhereRunningIfKnown).getManagementContext(), steps);
+            WorkflowStepResolution.resolveSteps( ((BrooklynObjectInternal)entityOrAdjunctWhereRunningIfKnown).getManagementContext(), steps, params.containsKey(WorkflowCommonConfig.OUTPUT.getName()) ? "has_output" : null);
         }
     }
 
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java
index d1925d2..e56d38e 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java
@@ -58,6 +58,7 @@
 import javax.annotation.Nullable;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -163,8 +164,10 @@
     public void validateStep(@Nullable ManagementContext mgmt, @Nullable WorkflowExecutionContext workflow) {
         super.validateStep(mgmt, workflow);
 
-        if (steps instanceof List) WorkflowStepResolution.resolveSteps(mgmt, (List<Object>) steps);
-        else if (steps!=null) throw new IllegalArgumentException("Workflow `steps` must be a list");
+        if (steps instanceof List) {
+            if (((List)steps).isEmpty()) throw new IllegalArgumentException("Workflow `steps` must be supplied for a custom or nested workflow");
+            WorkflowStepResolution.resolveSteps(mgmt, (List<Object>) steps, null);
+        } else if (steps!=null) throw new IllegalArgumentException("Workflow `steps` must be a list");
         else if (target!=null) throw new IllegalArgumentException("Workflow cannot take a `target` without `steps`");
     }
 
@@ -561,7 +564,10 @@
         return newWorkflowExecution(entity, name, extraConfig, null);
     }
     public WorkflowExecutionContext newWorkflowExecution(Entity entity, String name, ConfigBag extraConfig, Map extraTaskFlags) {
-        if (steps==null) throw new IllegalArgumentException("Cannot make new workflow with no steps");
+        if (steps==null) {
+            if (target!=null) throw new IllegalArgumentException("Steps are required for a workflow with a target");
+            else steps = MutableList.of();
+        }
 
         if (target==null) {
             // copy everything as we are going to run it "flat"
@@ -580,12 +586,15 @@
         }
     }
 
-    private ConfigBag getConfigForSubWorkflow(boolean includeInput) {
+    private ConfigBag getConfigForSubWorkflow(boolean isFlattened) {
+        if (isFlattened && (output!=null && workflowOutput!=null)) {
+            if (!Objects.equals(output, workflowOutput)) throw new IllegalArgumentException("Setting both 'output' and 'workflowOutput' is not supported for custom steps without target");
+        }
         ConfigBag result = ConfigBag.newInstance()
                 .configure(WorkflowCommonConfig.PARAMETER_DEFS, parameters)
                 .configure(WorkflowCommonConfig.STEPS, steps)
-                .configure(WorkflowCommonConfig.INPUT, includeInput ? input : null)  // input is resolved in outer workflow so it can reference outer workflow vars
-                .configure(WorkflowCommonConfig.OUTPUT, workflowOutput)
+                .configure(WorkflowCommonConfig.INPUT, isFlattened ? input : null)  // input is resolved in outer workflow so it can reference outer workflow vars
+                .configure(WorkflowCommonConfig.OUTPUT, isFlattened && workflowOutput==null ? output : workflowOutput)
                 .configure(WorkflowCommonConfig.RETENTION, retention)
                 .configure(WorkflowCommonConfig.REPLAYABLE, replayable)
                 .configure(WorkflowCommonConfig.IDEMPOTENT, idempotent)
diff --git a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowBasicTest.java b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowBasicTest.java
index 8d0ce20..9287cfb 100644
--- a/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowBasicTest.java
+++ b/core/src/test/java/org/apache/brooklyn/core/workflow/WorkflowBasicTest.java
@@ -216,7 +216,7 @@
                         "log test message"
                 );
 
-        List<WorkflowStepDefinition> steps = WorkflowStepResolution.resolveSteps(mgmt, stepsDefinition);
+        List<WorkflowStepDefinition> steps = WorkflowStepResolution.resolveSteps(mgmt, stepsDefinition, null);
         Asserts.assertSize(steps, 4);
     }
 
@@ -229,7 +229,7 @@
             Asserts.assertInstanceOf(wf, CustomWorkflowStep.class);
             Asserts.assertSize(((CustomWorkflowStep) wf).peekSteps(), 1);
             Asserts.assertInstanceOf(
-                    WorkflowStepResolution.resolveSteps( mgmt, ((CustomWorkflowStep) wf).peekSteps() ).get(0), LogWorkflowStep.class);
+                    WorkflowStepResolution.resolveSteps( mgmt, ((CustomWorkflowStep) wf).peekSteps(), null ).get(0), LogWorkflowStep.class);
         };
 
         test.accept( BeanWithTypeUtils.convert(mgmt,
@@ -480,5 +480,17 @@
                 w1.getTask(false).get().getUnchecked(),
                 MutableList.of("a=b", "b=c"));
     }
+
+    @Test
+    public void testOutputOnlyWorkflow() {
+        loadTypes();
+        BasicApplication app = mgmt.getEntityManager().createEntity(EntitySpec.create(BasicApplication.class));
+        WorkflowExecutionContext w1 = WorkflowBasicTest.runWorkflow(app, Strings.lines(
+                "output: 42"
+        ), null);
+        Asserts.assertEquals(
+                w1.getTask(false).get().getUnchecked(),
+                42);
+    }
     
 }