diff --git a/app/test-pmd/cmdline_flow.c b/app/test-pmd/cmdline_flow.c
index 26ef2ccfd4..b9edb1d482 100644
--- a/app/test-pmd/cmdline_flow.c
+++ b/app/test-pmd/cmdline_flow.c
@@ -121,6 +121,7 @@ enum index {
 	/* Queue arguments. */
 	QUEUE_CREATE,
 	QUEUE_DESTROY,
+	QUEUE_INDIRECT_ACTION,
 
 	/* Queue create arguments. */
 	QUEUE_CREATE_ID,
@@ -134,6 +135,26 @@ enum index {
 	QUEUE_DESTROY_ID,
 	QUEUE_DESTROY_POSTPONE,
 
+	/* Queue indirect action arguments */
+	QUEUE_INDIRECT_ACTION_CREATE,
+	QUEUE_INDIRECT_ACTION_UPDATE,
+	QUEUE_INDIRECT_ACTION_DESTROY,
+
+	/* Queue indirect action create arguments */
+	QUEUE_INDIRECT_ACTION_CREATE_ID,
+	QUEUE_INDIRECT_ACTION_INGRESS,
+	QUEUE_INDIRECT_ACTION_EGRESS,
+	QUEUE_INDIRECT_ACTION_TRANSFER,
+	QUEUE_INDIRECT_ACTION_CREATE_POSTPONE,
+	QUEUE_INDIRECT_ACTION_SPEC,
+
+	/* Queue indirect action update arguments */
+	QUEUE_INDIRECT_ACTION_UPDATE_POSTPONE,
+
+	/* Queue indirect action destroy arguments */
+	QUEUE_INDIRECT_ACTION_DESTROY_ID,
+	QUEUE_INDIRECT_ACTION_DESTROY_POSTPONE,
+
 	/* Push arguments. */
 	PUSH_QUEUE,
 
@@ -1102,6 +1123,7 @@ static const enum index next_table_destroy_attr[] = {
 static const enum index next_queue_subcmd[] = {
 	QUEUE_CREATE,
 	QUEUE_DESTROY,
+	QUEUE_INDIRECT_ACTION,
 	ZERO,
 };
 
@@ -1111,6 +1133,36 @@ static const enum index next_queue_destroy_attr[] = {
 	ZERO,
 };
 
+static const enum index next_qia_subcmd[] = {
+	QUEUE_INDIRECT_ACTION_CREATE,
+	QUEUE_INDIRECT_ACTION_UPDATE,
+	QUEUE_INDIRECT_ACTION_DESTROY,
+	ZERO,
+};
+
+static const enum index next_qia_create_attr[] = {
+	QUEUE_INDIRECT_ACTION_CREATE_ID,
+	QUEUE_INDIRECT_ACTION_INGRESS,
+	QUEUE_INDIRECT_ACTION_EGRESS,
+	QUEUE_INDIRECT_ACTION_TRANSFER,
+	QUEUE_INDIRECT_ACTION_CREATE_POSTPONE,
+	QUEUE_INDIRECT_ACTION_SPEC,
+	ZERO,
+};
+
+static const enum index next_qia_update_attr[] = {
+	QUEUE_INDIRECT_ACTION_UPDATE_POSTPONE,
+	QUEUE_INDIRECT_ACTION_SPEC,
+	ZERO,
+};
+
+static const enum index next_qia_destroy_attr[] = {
+	QUEUE_INDIRECT_ACTION_DESTROY_POSTPONE,
+	QUEUE_INDIRECT_ACTION_DESTROY_ID,
+	END,
+	ZERO,
+};
+
 static const enum index next_ia_create_attr[] = {
 	INDIRECT_ACTION_CREATE_ID,
 	INDIRECT_ACTION_INGRESS,
@@ -2167,6 +2219,12 @@ static int parse_qo(struct context *, const struct token *,
 static int parse_qo_destroy(struct context *, const struct token *,
 			    const char *, unsigned int,
 			    void *, unsigned int);
+static int parse_qia(struct context *, const struct token *,
+		     const char *, unsigned int,
+		     void *, unsigned int);
+static int parse_qia_destroy(struct context *, const struct token *,
+			     const char *, unsigned int,
+			     void *, unsigned int);
 static int parse_push(struct context *, const struct token *,
 		      const char *, unsigned int,
 		      void *, unsigned int);
@@ -2744,6 +2802,13 @@ static const struct token token_list[] = {
 		.args = ARGS(ARGS_ENTRY(struct buffer, queue)),
 		.call = parse_qo_destroy,
 	},
+	[QUEUE_INDIRECT_ACTION] = {
+		.name = "indirect_action",
+		.help = "queue indirect actions",
+		.next = NEXT(next_qia_subcmd, NEXT_ENTRY(COMMON_QUEUE_ID)),
+		.args = ARGS(ARGS_ENTRY(struct buffer, queue)),
+		.call = parse_qia,
+	},
 	/* Queue  arguments. */
 	[QUEUE_TEMPLATE_TABLE] = {
 		.name = "template table",
@@ -2797,6 +2862,90 @@ static const struct token token_list[] = {
 					    args.destroy.rule)),
 		.call = parse_qo_destroy,
 	},
+	/* Queue indirect action arguments */
+	[QUEUE_INDIRECT_ACTION_CREATE] = {
+		.name = "create",
+		.help = "create indirect action",
+		.next = NEXT(next_qia_create_attr),
+		.call = parse_qia,
+	},
+	[QUEUE_INDIRECT_ACTION_UPDATE] = {
+		.name = "update",
+		.help = "update indirect action",
+		.next = NEXT(next_qia_update_attr,
+			     NEXT_ENTRY(COMMON_INDIRECT_ACTION_ID)),
+		.args = ARGS(ARGS_ENTRY(struct buffer, args.vc.attr.group)),
+		.call = parse_qia,
+	},
+	[QUEUE_INDIRECT_ACTION_DESTROY] = {
+		.name = "destroy",
+		.help = "destroy indirect action",
+		.next = NEXT(next_qia_destroy_attr),
+		.call = parse_qia_destroy,
+	},
+	/* Indirect action destroy arguments. */
+	[QUEUE_INDIRECT_ACTION_DESTROY_POSTPONE] = {
+		.name = "postpone",
+		.help = "postpone destroy operation",
+		.next = NEXT(next_qia_destroy_attr,
+			     NEXT_ENTRY(COMMON_BOOLEAN)),
+		.args = ARGS(ARGS_ENTRY(struct buffer, postpone)),
+	},
+	[QUEUE_INDIRECT_ACTION_DESTROY_ID] = {
+		.name = "action_id",
+		.help = "specify a indirect action id to destroy",
+		.next = NEXT(next_qia_destroy_attr,
+			     NEXT_ENTRY(COMMON_INDIRECT_ACTION_ID)),
+		.args = ARGS(ARGS_ENTRY_PTR(struct buffer,
+					    args.ia_destroy.action_id)),
+		.call = parse_qia_destroy,
+	},
+	/* Indirect action update arguments. */
+	[QUEUE_INDIRECT_ACTION_UPDATE_POSTPONE] = {
+		.name = "postpone",
+		.help = "postpone update operation",
+		.next = NEXT(next_qia_update_attr,
+			     NEXT_ENTRY(COMMON_BOOLEAN)),
+		.args = ARGS(ARGS_ENTRY(struct buffer, postpone)),
+	},
+	/* Indirect action create arguments. */
+	[QUEUE_INDIRECT_ACTION_CREATE_ID] = {
+		.name = "action_id",
+		.help = "specify a indirect action id to create",
+		.next = NEXT(next_qia_create_attr,
+			     NEXT_ENTRY(COMMON_INDIRECT_ACTION_ID)),
+		.args = ARGS(ARGS_ENTRY(struct buffer, args.vc.attr.group)),
+	},
+	[QUEUE_INDIRECT_ACTION_INGRESS] = {
+		.name = "ingress",
+		.help = "affect rule to ingress",
+		.next = NEXT(next_qia_create_attr),
+		.call = parse_qia,
+	},
+	[QUEUE_INDIRECT_ACTION_EGRESS] = {
+		.name = "egress",
+		.help = "affect rule to egress",
+		.next = NEXT(next_qia_create_attr),
+		.call = parse_qia,
+	},
+	[QUEUE_INDIRECT_ACTION_TRANSFER] = {
+		.name = "transfer",
+		.help = "affect rule to transfer",
+		.next = NEXT(next_qia_create_attr),
+		.call = parse_qia,
+	},
+	[QUEUE_INDIRECT_ACTION_CREATE_POSTPONE] = {
+		.name = "postpone",
+		.help = "postpone create operation",
+		.next = NEXT(next_qia_create_attr,
+			     NEXT_ENTRY(COMMON_BOOLEAN)),
+		.args = ARGS(ARGS_ENTRY(struct buffer, postpone)),
+	},
+	[QUEUE_INDIRECT_ACTION_SPEC] = {
+		.name = "action",
+		.help = "specify action to create indirect handle",
+		.next = NEXT(next_action),
+	},
 	/* Top-level command. */
 	[PUSH] = {
 		.name = "push",
@@ -6209,6 +6358,110 @@ parse_ia_destroy(struct context *ctx, const struct token *token,
 	return len;
 }
 
+/** Parse tokens for indirect action commands. */
+static int
+parse_qia(struct context *ctx, const struct token *token,
+	  const char *str, unsigned int len,
+	  void *buf, unsigned int size)
+{
+	struct buffer *out = buf;
+
+	/* Token name must match. */
+	if (parse_default(ctx, token, str, len, NULL, 0) < 0)
+		return -1;
+	/* Nothing else to do if there is no buffer. */
+	if (!out)
+		return len;
+	if (!out->command) {
+		if (ctx->curr != QUEUE)
+			return -1;
+		if (sizeof(*out) > size)
+			return -1;
+		out->args.vc.data = (uint8_t *)out + size;
+		return len;
+	}
+	switch (ctx->curr) {
+	case QUEUE_INDIRECT_ACTION:
+		return len;
+	case QUEUE_INDIRECT_ACTION_CREATE:
+	case QUEUE_INDIRECT_ACTION_UPDATE:
+		out->args.vc.actions =
+			(void *)RTE_ALIGN_CEIL((uintptr_t)(out + 1),
+					       sizeof(double));
+		out->args.vc.attr.group = UINT32_MAX;
+		out->command = ctx->curr;
+		ctx->objdata = 0;
+		ctx->object = out;
+		ctx->objmask = NULL;
+		return len;
+	case QUEUE_INDIRECT_ACTION_EGRESS:
+		out->args.vc.attr.egress = 1;
+		return len;
+	case QUEUE_INDIRECT_ACTION_INGRESS:
+		out->args.vc.attr.ingress = 1;
+		return len;
+	case QUEUE_INDIRECT_ACTION_TRANSFER:
+		out->args.vc.attr.transfer = 1;
+		return len;
+	case QUEUE_INDIRECT_ACTION_CREATE_POSTPONE:
+		return len;
+	default:
+		return -1;
+	}
+}
+
+/** Parse tokens for indirect action destroy command. */
+static int
+parse_qia_destroy(struct context *ctx, const struct token *token,
+		  const char *str, unsigned int len,
+		  void *buf, unsigned int size)
+{
+	struct buffer *out = buf;
+	uint32_t *action_id;
+
+	/* Token name must match. */
+	if (parse_default(ctx, token, str, len, NULL, 0) < 0)
+		return -1;
+	/* Nothing else to do if there is no buffer. */
+	if (!out)
+		return len;
+	if (!out->command || out->command == QUEUE) {
+		if (ctx->curr != QUEUE_INDIRECT_ACTION_DESTROY)
+			return -1;
+		if (sizeof(*out) > size)
+			return -1;
+		out->command = ctx->curr;
+		ctx->objdata = 0;
+		ctx->object = out;
+		ctx->objmask = NULL;
+		out->args.ia_destroy.action_id =
+			(void *)RTE_ALIGN_CEIL((uintptr_t)(out + 1),
+					       sizeof(double));
+		return len;
+	}
+	switch (ctx->curr) {
+	case QUEUE_INDIRECT_ACTION:
+		out->command = ctx->curr;
+		ctx->objdata = 0;
+		ctx->object = out;
+		ctx->objmask = NULL;
+		return len;
+	case QUEUE_INDIRECT_ACTION_DESTROY_ID:
+		action_id = out->args.ia_destroy.action_id
+				+ out->args.ia_destroy.action_id_n++;
+		if ((uint8_t *)action_id > (uint8_t *)out + size)
+			return -1;
+		ctx->objdata = 0;
+		ctx->object = action_id;
+		ctx->objmask = NULL;
+		return len;
+	case QUEUE_INDIRECT_ACTION_DESTROY_POSTPONE:
+		return len;
+	default:
+		return -1;
+	}
+}
+
 /** Parse tokens for meter policy action commands. */
 static int
 parse_mp(struct context *ctx, const struct token *token,
@@ -9892,6 +10145,29 @@ cmd_flow_parsed(const struct buffer *in)
 	case PULL:
 		port_queue_flow_pull(in->port, in->queue);
 		break;
+	case QUEUE_INDIRECT_ACTION_CREATE:
+		port_queue_action_handle_create(
+				in->port, in->queue, in->postpone,
+				in->args.vc.attr.group,
+				&((const struct rte_flow_indir_action_conf) {
+					.ingress = in->args.vc.attr.ingress,
+					.egress = in->args.vc.attr.egress,
+					.transfer = in->args.vc.attr.transfer,
+				}),
+				in->args.vc.actions);
+		break;
+	case QUEUE_INDIRECT_ACTION_DESTROY:
+		port_queue_action_handle_destroy(in->port,
+					   in->queue, in->postpone,
+					   in->args.ia_destroy.action_id_n,
+					   in->args.ia_destroy.action_id);
+		break;
+	case QUEUE_INDIRECT_ACTION_UPDATE:
+		port_queue_action_handle_update(in->port,
+						in->queue, in->postpone,
+						in->args.vc.attr.group,
+						in->args.vc.actions);
+		break;
 	case INDIRECT_ACTION_CREATE:
 		port_action_handle_create(
 				in->port, in->args.vc.attr.group,
diff --git a/app/test-pmd/config.c b/app/test-pmd/config.c
index 4937851c41..e69dd2feff 100644
--- a/app/test-pmd/config.c
+++ b/app/test-pmd/config.c
@@ -2590,6 +2590,137 @@ port_queue_flow_destroy(portid_t port_id, queueid_t queue_id,
 	return ret;
 }
 
+/** Enqueue indirect action create operation. */
+int
+port_queue_action_handle_create(portid_t port_id, uint32_t queue_id,
+				bool postpone, uint32_t id,
+				const struct rte_flow_indir_action_conf *conf,
+				const struct rte_flow_action *action)
+{
+	const struct rte_flow_q_ops_attr attr = { .postpone = postpone};
+	struct rte_port *port;
+	struct port_indirect_action *pia;
+	int ret;
+	struct rte_flow_error error;
+
+	ret = action_alloc(port_id, id, &pia);
+	if (ret)
+		return ret;
+
+	port = &ports[port_id];
+	if (queue_id >= port->queue_nb) {
+		printf("Queue #%u is invalid\n", queue_id);
+		return -EINVAL;
+	}
+
+	if (action->type == RTE_FLOW_ACTION_TYPE_AGE) {
+		struct rte_flow_action_age *age =
+			(struct rte_flow_action_age *)(uintptr_t)(action->conf);
+
+		pia->age_type = ACTION_AGE_CONTEXT_TYPE_INDIRECT_ACTION;
+		age->context = &pia->age_type;
+	}
+	/* Poisoning to make sure PMDs update it in case of error. */
+	memset(&error, 0x88, sizeof(error));
+	pia->handle = rte_flow_q_action_handle_create(port_id, queue_id, &attr,
+						      conf, action, &error);
+	if (!pia->handle) {
+		uint32_t destroy_id = pia->id;
+		port_queue_action_handle_destroy(port_id, queue_id,
+						 postpone, 1, &destroy_id);
+		return port_flow_complain(&error);
+	}
+	pia->type = action->type;
+	printf("Indirect action #%u creation queued\n", pia->id);
+	return 0;
+}
+
+/** Enqueue indirect action destroy operation. */
+int
+port_queue_action_handle_destroy(portid_t port_id,
+				 uint32_t queue_id, bool postpone,
+				 uint32_t n, const uint32_t *actions)
+{
+	const struct rte_flow_q_ops_attr attr = { .postpone = postpone};
+	struct rte_port *port;
+	struct port_indirect_action **tmp;
+	uint32_t c = 0;
+	int ret = 0;
+
+	if (port_id_is_invalid(port_id, ENABLED_WARN) ||
+	    port_id == (portid_t)RTE_PORT_ALL)
+		return -EINVAL;
+	port = &ports[port_id];
+
+	if (queue_id >= port->queue_nb) {
+		printf("Queue #%u is invalid\n", queue_id);
+		return -EINVAL;
+	}
+
+	tmp = &port->actions_list;
+	while (*tmp) {
+		uint32_t i;
+
+		for (i = 0; i != n; ++i) {
+			struct rte_flow_error error;
+			struct port_indirect_action *pia = *tmp;
+
+			if (actions[i] != pia->id)
+				continue;
+			/*
+			 * Poisoning to make sure PMDs update it in case
+			 * of error.
+			 */
+			memset(&error, 0x99, sizeof(error));
+
+			if (pia->handle &&
+			    rte_flow_q_action_handle_destroy(port_id, queue_id,
+						&attr, pia->handle, &error)) {
+				ret = port_flow_complain(&error);
+				continue;
+			}
+			*tmp = pia->next;
+			printf("Indirect action #%u destruction queued\n",
+			       pia->id);
+			free(pia);
+			break;
+		}
+		if (i == n)
+			tmp = &(*tmp)->next;
+		++c;
+	}
+	return ret;
+}
+
+/** Enqueue indirect action update operation. */
+int
+port_queue_action_handle_update(portid_t port_id,
+				uint32_t queue_id, bool postpone, uint32_t id,
+				const struct rte_flow_action *action)
+{
+	const struct rte_flow_q_ops_attr attr = { .postpone = postpone};
+	struct rte_port *port;
+	struct rte_flow_error error;
+	struct rte_flow_action_handle *action_handle;
+
+	action_handle = port_action_handle_get_by_id(port_id, id);
+	if (!action_handle)
+		return -EINVAL;
+
+	port = &ports[port_id];
+	if (queue_id >= port->queue_nb) {
+		printf("Queue #%u is invalid\n", queue_id);
+		return -EINVAL;
+	}
+
+	if (rte_flow_q_action_handle_update(port_id, queue_id, &attr,
+					    action_handle, action, &error)) {
+		return port_flow_complain(&error);
+	}
+	printf("Indirect action #%u update queued\n", id);
+	return 0;
+}
+
 /** Push all the queue operations in the queue to the NIC. */
 int
 port_queue_flow_push(portid_t port_id, queueid_t queue_id)
diff --git a/app/test-pmd/testpmd.h b/app/test-pmd/testpmd.h
index 6fe829edab..167f1741dc 100644
--- a/app/test-pmd/testpmd.h
+++ b/app/test-pmd/testpmd.h
@@ -939,6 +939,16 @@ int port_queue_flow_create(portid_t port_id, queueid_t queue_id,
 			   const struct rte_flow_action *actions);
 int port_queue_flow_destroy(portid_t port_id, queueid_t queue_id,
 			    bool postpone, uint32_t n, const uint32_t *rule);
+int port_queue_action_handle_create(portid_t port_id, uint32_t queue_id,
+			bool postpone, uint32_t id,
+			const struct rte_flow_indir_action_conf *conf,
+			const struct rte_flow_action *action);
+int port_queue_action_handle_destroy(portid_t port_id,
+				     uint32_t queue_id, bool postpone,
+				     uint32_t n, const uint32_t *action);
+int port_queue_action_handle_update(portid_t port_id, uint32_t queue_id,
+				    bool postpone, uint32_t id,
+				    const struct rte_flow_action *action);
 int port_queue_flow_push(portid_t port_id, queueid_t queue_id);
 int port_queue_flow_pull(portid_t port_id, queueid_t queue_id);
 int port_flow_validate(portid_t port_id,
diff --git a/doc/guides/testpmd_app_ug/testpmd_funcs.rst b/doc/guides/testpmd_app_ug/testpmd_funcs.rst
index d5d9125d50..65ecef754e 100644
--- a/doc/guides/testpmd_app_ug/testpmd_funcs.rst
+++ b/doc/guides/testpmd_app_ug/testpmd_funcs.rst
@@ -4780,6 +4780,31 @@ port 0::
 	testpmd> flow indirect_action 0 create action_id \
 		ingress action rss queues 0 1 end / end
 
+Enqueueing creation of indirect actions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``flow queue indirect_action create`` adds creation operation of an indirect
+action to a queue. It is bound to ``rte_flow_q_action_handle_create()``::
+
+   flow queue {port_id} create {queue_id} [postpone {boolean}]
+       table {table_id} item_template {item_template_id}
+       action_template {action_template_id}
+       pattern {item} [/ {item} [...]] / end
+       actions {action} [/ {action} [...]] / end
+
+If successful, it will show::
+
+   Indirect action #[...] creation queued
+
+Otherwise it will show an error message of the form::
+
+   Caught error type [...] ([...]): [...]
+
+This command uses the same parameters as  ``flow indirect_action create``,
+described in `Creating indirect actions`_.
+
+``flow queue pull`` must be called to retrieve the operation status.
+
 Updating indirect actions
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -4809,6 +4834,25 @@ Update indirect rss action having id 100 on port 0 with rss to queues 0 and 3
 
    testpmd> flow indirect_action 0 update 100 action rss queues 0 3 end / end
 
+Enqueueing update of indirect actions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``flow queue indirect_action update`` adds update operation for an indirect
+action to a queue. It is bound to ``rte_flow_q_action_handle_update()``::
+
+   flow queue {port_id} indirect_action {queue_id} update
+      {indirect_action_id} [postpone {boolean}] action {action} / end
+
+If successful, it will show::
+
+   Indirect action #[...] update queued
+
+Otherwise it will show an error message of the form::
+
+   Caught error type [...] ([...]): [...]
+
+``flow queue pull`` must be called to retrieve the operation status.
+
 Destroying indirect actions
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -4832,6 +4876,27 @@ Destroy indirect actions having id 100 & 101::
 
    testpmd> flow indirect_action 0 destroy action_id 100 action_id 101
 
+Enqueueing destruction of indirect actions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``flow queue indirect_action destroy`` adds destruction operation to destroy
+one or more indirect actions from their indirect action IDs (as returned by
+``flow queue {port_id} indirect_action {queue_id} create``) to a queue.
+It is bound to ``rte_flow_q_action_handle_destroy()``::
+
+   flow queue {port_id} indirect_action {queue_id} destroy
+      [postpone {boolean}] action_id {indirect_action_id} [...]
+
+If successful, it will show::
+
+   Indirect action #[...] destruction queued
+
+Otherwise it will show an error message of the form::
+
+   Caught error type [...] ([...]): [...]
+
+``flow queue pull`` must be called to retrieve the operation status.
+
 Query indirect actions
 ~~~~~~~~~~~~~~~~~~~~~~
 
