diff --git a/UserManual.md b/UserManual.md
index 4b2536a..0a796a7 100644
--- a/UserManual.md
+++ b/UserManual.md
@@ -104,16 +104,35 @@ chmod +x /var/lib/laminar/cfg/hello.run
 
 # Triggering a run
 
-To trigger a run of the `hello` job, execute
+When triggering a run, the job is first added to a queue of upcoming tasks. If the server is busy, the job may wait in this queue for a while. It will only be assigned a job number when it leaves this queue and starts executing. The job number may be useful to the client that triggers the run, so there are a few ways to trigger a run.
+
+To add the `hello` job to the queue ("fire-and-forget"), execute
 
 ```bash
-laminarc trigger hello
+laminarc queue hello
 ```
 
-This causes the `/var/lib/laminar/cfg/hello.run` script to be executed, with a working directory of `/var/lib/laminar/run/hello/1`
+In this case, laminarc returns immediately, with its error code indicating whether adding the job to the queue was sucessful.
+
+To queue the job and wait until it leaves the queue and starts executing, use
+
+```bash
+laminarc start hello
+```
+
+In this case, laminarc blocks until the job starts executing, or returns immediately if queueing failed. The run number will be printed to standard output.
+
+To launch and run the `hello` job to completion, execute
+
+```bash
+laminarc run hello
+```
+
+In all cases, a started run means the `/var/lib/laminar/cfg/hello.run` script will be executed, with a working directory of `/var/lib/laminar/run/hello/1` (or current run number)
 
 The result and log output should be visible in the Web UI at http://localhost:8080/jobs/hello/1
 
+
 ## Isn't there a "Build Now" button I can click?
 
 This is against the design principles of Laminar and was deliberately excluded. Laminar's web UI is strictly read-only, making it simple to deploy in mixed-permission or public environments without an authentication layer. Furthermore, Laminar tries to encourage ideal continuous integration, where manual triggering is an anti-pattern. Want to make a release? Push a git tag and implement a post-receive hook. Want to re-run a build due to sporadic failure/flaky tests? Fix the tests locally and push a patch. Experience shows that a manual trigger such as a "Build Now" button is often used as a crutch to avoid doing the correct thing, negatively impacting traceability and quality.
diff --git a/src/client.cpp b/src/client.cpp
index e1835c0..abbb18e 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -84,16 +84,16 @@ int main(int argc, char** argv) {
 
     auto& waitScope = client.getWaitScope();
 
-    if(strcmp(argv[1], "trigger") == 0) {
+    if(strcmp(argv[1], "queue") == 0) {
         if(argc < 3) {
-            fprintf(stderr, "Usage %s trigger <jobName>\n", argv[0]);
+            fprintf(stderr, "Usage %s queue <jobName>\n", argv[0]);
             return EINVAL;
         }
-        kj::Vector<capnp::RemotePromise<LaminarCi::TriggerResults>> promises;
+        kj::Vector<capnp::RemotePromise<LaminarCi::QueueResults>> promises;
         int jobNameIndex = 2;
         // make a request for each job specified on the commandline
         do {
-            auto req = laminar.triggerRequest();
+            auto req = laminar.queueRequest();
             req.setJobName(argv[jobNameIndex]);
             int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
             promises.add(req.send());
@@ -106,14 +106,39 @@ int main(int argc, char** argv) {
                 return ENOENT;
             }
         }
-    } else if(strcmp(argv[1], "run") == 0 || strcmp(argv[1], "start") == 0) {
+    } else if(strcmp(argv[1], "start") == 0 || strcmp(argv[1], "trigger") == 0) {
+        if(strcmp(argv[1], "trigger") == 0)
+            fprintf(stderr, "Warning: 'trigger' is deprecated, use 'queue' for the old behavior\n");
+        if(argc < 3) {
+            fprintf(stderr, "Usage %s queue <jobName>\n", argv[0]);
+            return EINVAL;
+        }
+        kj::Vector<capnp::RemotePromise<LaminarCi::StartResults>> promises;
+        struct: public kj::TaskSet::ErrorHandler {
+            void taskFailed(kj::Exception&&) override {}
+        } ignoreFailed;
+        kj::TaskSet ts(ignoreFailed);
+        int jobNameIndex = 2;
+        // make a request for each job specified on the commandline
+        do {
+            auto req = laminar.startRequest();
+            req.setJobName(argv[jobNameIndex]);
+            int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req);
+            ts.add(req.send().then([&ret,argv,jobNameIndex](capnp::Response<LaminarCi::StartResults> resp){
+                if(resp.getResult() != LaminarCi::MethodResult::SUCCESS) {
+                    fprintf(stderr, "Failed to start job '%s'\n", argv[2]);
+                    ret = ENOENT;
+                }
+                printf("%s:%d\n", argv[jobNameIndex], resp.getBuildNum());
+            }));
+            jobNameIndex += n + 1;
+        } while(jobNameIndex < argc);
+        ts.onEmpty().wait(waitScope);
+    } else if(strcmp(argv[1], "run") == 0) {
         if(argc < 3) {
             fprintf(stderr, "Usage %s run <jobName>\n", argv[0]);
             return EINVAL;
         }
-        if(strcmp(argv[1], "start") == 0) {
-            fprintf(stderr, "Warning: \"start\" is deprecated, please use \"run\" instead\n");
-        }
         struct: public kj::TaskSet::ErrorHandler {
             void taskFailed(kj::Exception&&) override {}
         } ignoreFailed;
diff --git a/src/laminar.capnp b/src/laminar.capnp
index 3b467f2..a5701f4 100644
--- a/src/laminar.capnp
+++ b/src/laminar.capnp
@@ -2,11 +2,12 @@
 
 interface LaminarCi {
 
-    trigger @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult);
-    run @1 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32);
-    set @2 (jobName :Text, buildNum :UInt32, param :JobParam) -> (result :MethodResult);
-    lock @3 (lockName :Text) -> ();
-    release @4 (lockName :Text) -> ();
+    queue @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult);
+    start @1 (jobName :Text, params :List(JobParam)) -> (result :MethodResult, buildNum :UInt32);
+    run @2 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32);
+    set @3 (jobName :Text, buildNum :UInt32, param :JobParam) -> (result :MethodResult);
+    lock @4 (lockName :Text) -> ();
+    release @5 (lockName :Text) -> ();
 
     struct JobParam {
         name @0 :Text;
diff --git a/src/laminar.cpp b/src/laminar.cpp
index cd685be..5121d90 100644
--- a/src/laminar.cpp
+++ b/src/laminar.cpp
@@ -712,6 +712,9 @@ void Laminar::assignNewJobs() {
                         c->sendMessage(msg);
                 }
 
+                // notify the rpc client if the start command was used
+                run->started.fulfiller->fulfill();
+
                 // setup run completion handler
                 run->notifyCompletion = [this](Run* r) { runFinished(r); };
 
diff --git a/src/run.h b/src/run.h
index 59bcc02..d8e7e3e 100644
--- a/src/run.h
+++ b/src/run.h
@@ -92,6 +92,7 @@ public:
     int fd;
     std::unordered_map<std::string, std::string> params;
     kj::Promise<void> timeout = kj::NEVER_DONE;
+    kj::PromiseFulfillerPair<void> started = kj::newPromiseAndFulfiller<void>();
 
     time_t queuedAt;
     time_t startedAt;
diff --git a/src/server.cpp b/src/server.cpp
index 6f6848a..419418c 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -90,10 +90,10 @@ public:
         laminar.deregisterWaiter(this);
     }
 
-    // Start a job, without waiting for it to finish
-    kj::Promise<void> trigger(TriggerContext context) override {
+    // Queue a job, without waiting for it to start
+    kj::Promise<void> queue(QueueContext context) override {
         std::string jobName = context.getParams().getJobName();
-        LLOG(INFO, "RPC trigger", jobName);
+        LLOG(INFO, "RPC queue", jobName);
         ParamMap params;
         for(auto p : context.getParams().getParams()) {
             params[p.getName().cStr()] = p.getValue().cStr();
@@ -105,6 +105,26 @@ public:
         return kj::READY_NOW;
     }
 
+    // Start a job, without waiting for it to finish
+    kj::Promise<void> start(StartContext context) override {
+        std::string jobName = context.getParams().getJobName();
+        LLOG(INFO, "RPC start", jobName);
+        ParamMap params;
+        for(auto p : context.getParams().getParams()) {
+            params[p.getName().cStr()] = p.getValue().cStr();
+        }
+        std::shared_ptr<Run> run = laminar.queueJob(jobName, params);
+        if(Run* r = run.get()) {
+            return r->started.promise.then([context,r]() mutable {
+                context.getResults().setResult(LaminarCi::MethodResult::SUCCESS);
+                context.getResults().setBuildNum(r->build);
+            });
+        } else {
+            context.getResults().setResult(LaminarCi::MethodResult::FAILED);
+            return kj::READY_NOW;
+        }
+    }
+
     // Start a job and wait for the result
     kj::Promise<void> run(RunContext context) override {
         std::string jobName = context.getParams().getJobName();
@@ -115,11 +135,10 @@ public:
         }
         std::shared_ptr<Run> run = laminar.queueJob(jobName, params);
         if(const Run* r = run.get()) {
-            uint num = r->build;
             runWaiters[r].emplace_back(kj::newPromiseAndFulfiller<RunState>());
-            return runWaiters[r].back().promise.then([context,num](RunState state) mutable {
+            return runWaiters[r].back().promise.then([context,run](RunState state) mutable {
                 context.getResults().setResult(fromRunState(state));
-                context.getResults().setBuildNum(num);
+                context.getResults().setBuildNum(run->build);
             });
         } else {
             context.getResults().setResult(LaminarCi::JobResult::UNKNOWN);
diff --git a/test/test-server.cpp b/test/test-server.cpp
index 555b594..27b4187 100644
--- a/test/test-server.cpp
+++ b/test/test-server.cpp
@@ -98,8 +98,8 @@ protected:
     Server* server;
 };
 
-TEST_F(ServerTest, RpcTrigger) {
-    auto req = client().triggerRequest();
+TEST_F(ServerTest, RpcQueue) {
+    auto req = client().queueRequest();
     req.setJobName("foo");
     EXPECT_CALL(mockLaminar, queueJob("foo", ParamMap())).Times(testing::Exactly(1));
     req.send().wait(ws());